From b3024484be3d58e9c2a2eddb40e6a2b2d8476823 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 22 May 2025 16:39:08 +0530 Subject: [PATCH 01/44] record_type_add command added --- .../src/keepercli/commands/record_type.py | 71 +++++++++++++++++++ .../src/keepercli/register_commands.py | 5 +- .../keepersdk/vault/record_type_management.py | 39 ++++++++++ .../unit_tests/test_sync_down.py | 45 +++++++++++- 4 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/record_type.py create mode 100644 keepersdk-package/src/keepersdk/vault/record_type_management.py diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py new file mode 100644 index 00000000..51fa5ac0 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -0,0 +1,71 @@ +import argparse +import json +import logging + +from keepersdk.vault import record_type_management + +from . import base +from ..params import KeeperParams + +class RecordTypeAddCommand(base.ArgparseCommand): + parser = argparse.ArgumentParser( + prog='record-type-add', + description='Create a custom record type.' + ) + parser.add_argument( + '--data', + dest='data', + action='store', + required=True, + help='Record type definition in JSON format.' + ) + + def __init__(self): + super().__init__(RecordTypeAddCommand.parser) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + data = kwargs.get('data') + if not data: + raise ValueError("Cannot add record type without definition. Option --data is required.") + + try: + record_type = json.loads(data) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON format in --data: {e}") + + title = record_type.get('$id') + fields = record_type.get('fields') + description = record_type.get('description', '') + scope = record_type.get('scope', 'enterprise') + + if not title: + raise ValueError("Record type must have a '$id' field.") + if not fields or not isinstance(fields, list): + raise ValueError("Record type must include a list of 'fields'.") + + # Implicit fields - always present on any record, no need to be specified in the template: title, custom, notes + implicit_field_names = [x for x in record_implicit_fields] + implicit_fields = [r for r in record_type if r in implicit_field_names] + if implicit_fields: + error = {'error: Implicit fields not allowed in record type definition: ' + str(implicit_fields)} + raise ValueError(error) + + rt_attributes = ('$id', 'categories', 'description', 'fields') + bada = [r for r in record_type if r not in rt_attributes and r not in implicit_field_names] + if bada: + logging.debug(f'Unknown attributes in record type definition: {bada}') + + result = record_type_management.create_custom_record_type( + context.vault, title, fields, description + ) + print(result) + + +record_implicit_fields = { + 'title': '', # string + 'custom': [], # Array of Field Data objects + 'notes': '' # string +} \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 9a74f671..2fee42fb 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -23,7 +23,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): - from .commands import vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch + from .commands import vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, record_type commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) commands.register_command('ls', vault_folder.FolderListCommand(), base.CommandScope.Vault) @@ -42,6 +42,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('import', importer_commands.ImportCommand(), base.CommandScope.Vault) commands.register_command('export', importer_commands.ExportCommand(), base.CommandScope.Vault) commands.register_command('breachwatch', breachwatch.BreachWatchCommand(), base.CommandScope.Vault, 'bw') + commands.register_command('record-type-add', record_type.RecordTypeAddCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): @@ -57,4 +58,4 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('audit-report', audit_report.EnterpriseAuditReport(), base.CommandScope.Enterprise) commands.register_command('audit-alert', audit_alert.AuditAlerts(), base.CommandScope.Enterprise) commands.register_command('download-membership', importer_commands.DownloadMembershipCommand(), base.CommandScope.Enterprise) - commands.register_command('apply-membership', importer_commands.ApplyMembershipCommand(), base.CommandScope.Enterprise) + commands.register_command('apply-membership', importer_commands.ApplyMembershipCommand(), base.CommandScope.Enterprise) \ No newline at end of file diff --git a/keepersdk-package/src/keepersdk/vault/record_type_management.py b/keepersdk-package/src/keepersdk/vault/record_type_management.py new file mode 100644 index 00000000..98e074dd --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/record_type_management.py @@ -0,0 +1,39 @@ +import json + +from typing import List, Dict + +from . import vault_online, record_types +from ..proto import record_pb2 + + +def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str) -> str: + is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin + if not is_enterprise_admin: + raise ValueError('This command is restricted to Keeper Enterprise administrators.') + + if not fields: + raise ValueError('At least one field must be specified.') + + field_definitions = [] + for field in fields: + field_name = field.get("$ref") + if not field_name: + raise ValueError("Each field must contain a '$ref' key.") + if field_name not in record_types.FieldTypes: + raise ValueError(f"Field '{field_name}' is not a valid RecordField.") + field_definitions.append({"$ref": field_name}) + + record_type_data = { + "$id": title, + "description": description, + "categories": ["note"], + "fields": field_definitions + } + + request_payload = record_pb2.RecordType() + request_payload.content = json.dumps(record_type_data) + request_payload.scope = record_pb2.RecordTypeScope.RT_ENTERPRISE + + response = vault.keeper_auth.execute_auth_rest('vault/record_type_add', request_payload, response_type=record_pb2.RecordTypeModifyResponse) + + return f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in field_definitions]} and recordTypeId: {response.recordTypeId}" \ No newline at end of file diff --git a/keepersdk-package/unit_tests/test_sync_down.py b/keepersdk-package/unit_tests/test_sync_down.py index ca6fb9c6..681deca3 100644 --- a/keepersdk-package/unit_tests/test_sync_down.py +++ b/keepersdk-package/unit_tests/test_sync_down.py @@ -5,7 +5,7 @@ import data_vault from keepersdk import crypto, utils from keepersdk.proto import record_pb2 -from keepersdk.vault import vault_online, sync_down, memory_storage +from keepersdk.vault import vault_online, sync_down, memory_storage, record_type_management from keepersdk.proto import SyncDown_pb2 @@ -129,5 +129,48 @@ def test_delete_shared_folder(self): self.assertIsNone(vault.vault_data.get_shared_folder(shared_folder_uid)) +class CreateCustomRecordTypeTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.vault.keeper_auth.execute_auth_rest = MagicMock() + self.vault.keeper_auth.auth_context.is_enterprise_admin = True + + def test_successful_creation(self): + title = "TestType" + fields = [{"$ref": "login"}] + description = "A test type" + record_type_management.record_types.FieldTypes = {"login": {}} + result = record_type_management.create_custom_record_type(self.vault, title, fields, description) + self.assertIn("created successfully", result) + self.vault.keeper_auth.execute_auth_rest.assert_called_once_with( + 'vault/record_type_add', + unittest.mock.ANY, + response_type=record_pb2.RecordTypeModifyResponse + ) + + def test_not_enterprise_admin(self): + self.vault.keeper_auth.auth_context.is_enterprise_admin = False + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [{"$ref": "login"}], "desc") + self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) + + def test_missing_fields(self): + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [], "desc") + self.assertIn("At least one field", str(cm.exception)) + + def test_missing_ref(self): + record_type_management.record_types.FieldTypes = {"login": {}} + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [{}], "desc") + self.assertIn("Each field must contain a '$ref'", str(cm.exception)) + + def test_invalid_field_name(self): + record_type_management.record_types.FieldTypes = {"login": {}} + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [{"$ref": "not_a_field"}], "desc") + self.assertIn("is not a valid RecordField", str(cm.exception)) + + if __name__ == "__main__": unittest.main() From cf7532f43cc03542743bab9e1e0521a83da68d28 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 27 May 2025 20:39:05 +0530 Subject: [PATCH 02/44] PR review changes, fido upgrade change, login error eddress --- .../src/keepercli/commands/record_type.py | 12 ++++++++++-- keepersdk-package/requirements.txt | 2 +- keepersdk-package/setup.cfg | 3 ++- .../keepersdk/authentication/configuration.py | 2 +- .../src/keepersdk/authentication/yubikey.py | 18 +++++++++++------- .../keepersdk/vault/record_type_management.py | 6 +++--- keepersdk-package/unit_tests/test_sync_down.py | 3 ++- 7 files changed, 30 insertions(+), 16 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py index 51fa5ac0..e8a8c2a3 100644 --- a/keepercli-package/src/keepercli/commands/record_type.py +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -28,6 +28,15 @@ def execute(self, context: KeeperParams, **kwargs) -> None: raise ValueError("Vault is not initialized.") data = kwargs.get('data') + record_type = None + + if data and data.strip().startswith('filepath:'): + filepath = data.split('filepath:')[1].strip() + try: + with open(filepath, 'r') as file: + data = file.read() + except FileNotFoundError: + raise ValueError(f"File not found: {filepath}") if not data: raise ValueError("Cannot add record type without definition. Option --data is required.") @@ -39,7 +48,6 @@ def execute(self, context: KeeperParams, **kwargs) -> None: title = record_type.get('$id') fields = record_type.get('fields') description = record_type.get('description', '') - scope = record_type.get('scope', 'enterprise') if not title: raise ValueError("Record type must have a '$id' field.") @@ -61,7 +69,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: result = record_type_management.create_custom_record_type( context.vault, title, fields, description ) - print(result) + print(f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") record_implicit_fields = { diff --git a/keepersdk-package/requirements.txt b/keepersdk-package/requirements.txt index 7e24dd55..1a8481d0 100644 --- a/keepersdk-package/requirements.txt +++ b/keepersdk-package/requirements.txt @@ -4,4 +4,4 @@ requests>=2.31.0 cryptography>=41.0.7 protobuf>=5.28.3 websockets>=12.0 -fido2 \ No newline at end of file +fido2>=2.0.0; python_version>='3.10' diff --git a/keepersdk-package/setup.cfg b/keepersdk-package/setup.cfg index 3169bbb5..4e06773c 100644 --- a/keepersdk-package/setup.cfg +++ b/keepersdk-package/setup.cfg @@ -30,7 +30,8 @@ install_requires = cryptography>=40.0.0 protobuf>=4.25.0 websockets>=12.0 - fido2 + fido2>=2.0.0; python_version>='3.10' + [options.package_data] keepersdk = diff --git a/keepersdk-package/src/keepersdk/authentication/configuration.py b/keepersdk-package/src/keepersdk/authentication/configuration.py index 50fe8139..f360af4f 100644 --- a/keepersdk-package/src/keepersdk/authentication/configuration.py +++ b/keepersdk-package/src/keepersdk/authentication/configuration.py @@ -726,7 +726,7 @@ class JsonConfigurationStorage(IConfigurationStorage): def __init__(self, loader: Optional[IJsonLoader]=None) -> None: IConfigurationStorage.__init__(self) if not loader: - loader = JsonFileLoader('authentication.json') + loader = JsonFileLoader('config.json') self.loader = loader @classmethod diff --git a/keepersdk-package/src/keepersdk/authentication/yubikey.py b/keepersdk-package/src/keepersdk/authentication/yubikey.py index 5677d056..5c6a4600 100644 --- a/keepersdk-package/src/keepersdk/authentication/yubikey.py +++ b/keepersdk-package/src/keepersdk/authentication/yubikey.py @@ -1,12 +1,13 @@ import abc import json +import os import threading from typing import Optional, Any, Dict -from fido2.client import Fido2Client, WindowsClient, ClientError, WebAuthnClient, UserInteraction +from fido2.client import ClientError, DefaultClientDataCollector, UserInteraction, WebAuthnClient from fido2.ctap import CtapError from fido2.hid import CtapHidDevice -from fido2.webauthn import PublicKeyCredentialRequestOptions, UserVerificationRequirement, AuthenticatorAssertionResponse +from fido2.webauthn import PublicKeyCredentialRequestOptions, UserVerificationRequirement, AuthenticationResponse from .. import utils class IKeeperUserInteraction(abc.ABC): @@ -39,15 +40,18 @@ def yubikey_authenticate(request: Dict[str, Any], user_interaction: UserInteract if isinstance(challenge, str): options['challenge'] = utils.base64_url_decode(challenge) - client: WebAuthnClient - if WindowsClient.is_available(): - client = WindowsClient(origin, verify=verify_rp_id_none) + client = None # type: Optional[WebAuthnClient] + data_collector = DefaultClientDataCollector(origin, verify=verify_rp_id_none) + if os.name == 'nt': + from fido2.client.windows import WindowsClient + client = WindowsClient(client_data_collector=data_collector) else: dev = next(CtapHidDevice.list_devices(), None) if not dev: logger.warning("No Security Key detected") return None - fido_client = Fido2Client(dev, origin, verify=verify_rp_id_none, user_interaction=user_interaction) + from fido2.client import Fido2Client + fido_client = Fido2Client(dev, client_data_collector=data_collector, user_interaction=user_interaction) uv_configured = any(fido_client.info.options.get(k) for k in ("uv", "clientPin", "bioEnroll")) if not uv_configured: uv = options['userVerification'] @@ -56,7 +60,7 @@ def yubikey_authenticate(request: Dict[str, Any], user_interaction: UserInteract client = fido_client evt= threading.Event() - response: Optional[AuthenticatorAssertionResponse] = None + response: Optional[AuthenticationResponse] = None try: try: rq_options = PublicKeyCredentialRequestOptions.from_dict(options) diff --git a/keepersdk-package/src/keepersdk/vault/record_type_management.py b/keepersdk-package/src/keepersdk/vault/record_type_management.py index 98e074dd..c019552c 100644 --- a/keepersdk-package/src/keepersdk/vault/record_type_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_type_management.py @@ -6,7 +6,7 @@ from ..proto import record_pb2 -def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str) -> str: +def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str): is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin if not is_enterprise_admin: raise ValueError('This command is restricted to Keeper Enterprise administrators.') @@ -19,7 +19,7 @@ def create_custom_record_type(vault: vault_online.VaultOnline, title: str, field field_name = field.get("$ref") if not field_name: raise ValueError("Each field must contain a '$ref' key.") - if field_name not in record_types.FieldTypes: + if field_name not in record_types.FieldTypes and field_name not in record_types.RecordFields: raise ValueError(f"Field '{field_name}' is not a valid RecordField.") field_definitions.append({"$ref": field_name}) @@ -36,4 +36,4 @@ def create_custom_record_type(vault: vault_online.VaultOnline, title: str, field response = vault.keeper_auth.execute_auth_rest('vault/record_type_add', request_payload, response_type=record_pb2.RecordTypeModifyResponse) - return f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in field_definitions]} and recordTypeId: {response.recordTypeId}" \ No newline at end of file + return response diff --git a/keepersdk-package/unit_tests/test_sync_down.py b/keepersdk-package/unit_tests/test_sync_down.py index 681deca3..f271f0a9 100644 --- a/keepersdk-package/unit_tests/test_sync_down.py +++ b/keepersdk-package/unit_tests/test_sync_down.py @@ -140,8 +140,9 @@ def test_successful_creation(self): fields = [{"$ref": "login"}] description = "A test type" record_type_management.record_types.FieldTypes = {"login": {}} + self.vault.keeper_auth.execute_auth_rest.return_value = record_pb2.RecordTypeModifyResponse() result = record_type_management.create_custom_record_type(self.vault, title, fields, description) - self.assertIn("created successfully", result) + self.assertIsInstance(result, record_pb2.RecordTypeModifyResponse) self.vault.keeper_auth.execute_auth_rest.assert_called_once_with( 'vault/record_type_add', unittest.mock.ANY, From 83c781c55da19e5bc17df7d11cb7cdfc94e2aa31 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 28 May 2025 10:22:31 +0530 Subject: [PATCH 03/44] Added logger --- keepercli-package/src/keepercli/commands/record_type.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py index e8a8c2a3..0db2a606 100644 --- a/keepercli-package/src/keepercli/commands/record_type.py +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -6,6 +6,9 @@ from . import base from ..params import KeeperParams +from .. import api + +logger = api.get_logger() class RecordTypeAddCommand(base.ArgparseCommand): parser = argparse.ArgumentParser( @@ -69,7 +72,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: result = record_type_management.create_custom_record_type( context.vault, title, fields, description ) - print(f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") + logger.info(f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") record_implicit_fields = { From 093cd1c24ac81073264b8ac188d37fed5152e842 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 29 May 2025 19:58:08 +0530 Subject: [PATCH 04/44] record_type_edit and record_type_delete functions added --- .../src/keepercli/commands/record_type.py | 176 +++++++++++++----- .../src/keepercli/register_commands.py | 2 + .../keepersdk/vault/record_type_management.py | 73 +++++++- .../unit_tests/test_record_type_management.py | 140 ++++++++++++++ .../unit_tests/test_sync_down.py | 44 ----- 5 files changed, 344 insertions(+), 91 deletions(-) create mode 100644 keepersdk-package/unit_tests/test_record_type_management.py diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py index 0db2a606..643da9c6 100644 --- a/keepercli-package/src/keepercli/commands/record_type.py +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -11,72 +11,158 @@ logger = api.get_logger() class RecordTypeAddCommand(base.ArgparseCommand): - parser = argparse.ArgumentParser( - prog='record-type-add', - description='Create a custom record type.' - ) - parser.add_argument( - '--data', - dest='data', - action='store', - required=True, - help='Record type definition in JSON format.' - ) def __init__(self): - super().__init__(RecordTypeAddCommand.parser) + parser = argparse.ArgumentParser( + prog='record-type-add', + description='Add a new custom record type.' + ) + parser.add_argument( + '--data', + dest='data', + action='store', + required=True, + help='Record type definition in JSON format or "filepath:" to read from JSON file.' + ) + super().__init__(parser) def execute(self, context: KeeperParams, **kwargs) -> None: if not context.vault: raise ValueError("Vault is not initialized.") data = kwargs.get('data') - record_type = None - - if data and data.strip().startswith('filepath:'): - filepath = data.split('filepath:')[1].strip() - try: - with open(filepath, 'r') as file: - data = file.read() - except FileNotFoundError: - raise ValueError(f"File not found: {filepath}") - if not data: - raise ValueError("Cannot add record type without definition. Option --data is required.") - try: - record_type = json.loads(data) - except json.JSONDecodeError as e: - raise ValueError(f"Invalid JSON format in --data: {e}") + record_type = load_data(data) + + is_valid_data(record_type) title = record_type.get('$id') fields = record_type.get('fields') description = record_type.get('description', '') + categories = record_type.get('categories', []) - if not title: - raise ValueError("Record type must have a '$id' field.") - if not fields or not isinstance(fields, list): - raise ValueError("Record type must include a list of 'fields'.") + result = record_type_management.create_custom_record_type( + context.vault, title, fields, description, categories + ) + logger.info(f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") - # Implicit fields - always present on any record, no need to be specified in the template: title, custom, notes - implicit_field_names = [x for x in record_implicit_fields] - implicit_fields = [r for r in record_type if r in implicit_field_names] - if implicit_fields: - error = {'error: Implicit fields not allowed in record type definition: ' + str(implicit_fields)} - raise ValueError(error) - rt_attributes = ('$id', 'categories', 'description', 'fields') - bada = [r for r in record_type if r not in rt_attributes and r not in implicit_field_names] - if bada: - logging.debug(f'Unknown attributes in record type definition: {bada}') +class RecordTypeEditCommand(base.ArgparseCommand): - result = record_type_management.create_custom_record_type( - context.vault, title, fields, description + def __init__(self): + parser = argparse.ArgumentParser( + prog='record-type-edit', + description='Update or edit a custom record type.' ) - logger.info(f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") + parser.add_argument( + '--data', + dest='data', + action='store', + required=True, + help='Record type definition in JSON format or "filepath:" to read from JSON file.' + ) + parser.add_argument( + 'record_type_id', + type=int, + nargs='?', + help='Record Type ID of record type to be updated.' + ) + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + data = kwargs.get('data') + record_type_id = kwargs.get('record_type_id') + + if not record_type_id: + raise ValueError("Missing required argument: record_type_id") + + record_type = load_data(data) + + is_valid_data(record_type) + + title = record_type.get('$id') + fields = record_type.get('fields') + description = record_type.get('description', '') + categories = record_type.get('categories', []) + + result = record_type_management.edit_custom_record_types( + context.vault, record_type_id, title, fields, description, categories + ) + logger.info(f"Custom record type (ID: {record_type_id}) updated successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") + + +class RecordTypeDeleteCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='record-type-delete', + description='Delete a custom record type.' + ) + parser.add_argument( + 'record_type_id', + type=int, + help='Record Type ID of record type to be deleted.' + ) + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + record_type_id = kwargs.get('record_type_id') + if not record_type_id: + raise ValueError("Missing required argument: record_type_id.") + + result = record_type_management.delete_custom_record_types(context.vault, record_type_id) + logger.info(f"Custom record type deleted successfully with record type id: {result.recordTypeId}") record_implicit_fields = { 'title': '', # string 'custom': [], # Array of Field Data objects 'notes': '' # string -} \ No newline at end of file +} + + +def is_valid_data(record_type): + title = record_type.get('$id') + fields = record_type.get('fields') + + if not title: + raise ValueError("Record type must have a '$id' field.") + if not fields or not isinstance(fields, list): + raise ValueError("Record type must include a list of 'fields'.") + + # Implicit fields - always present on any record, no need to be specified in the template: title, custom, notes + implicit_field_names = [x for x in record_implicit_fields] + implicit_fields = [r for r in record_type if r in implicit_field_names] + if implicit_fields: + error = {'error: Implicit fields not allowed in record type definition: ' + str(implicit_fields)} + raise ValueError(error) + + rt_attributes = ('$id', 'categories', 'description', 'fields') + bad_attributes = [r for r in record_type if r not in rt_attributes and r not in implicit_field_names] + if bad_attributes: + logging.debug(f'Unknown attributes in record type definition: {bad_attributes}') + + +def load_data(data): + + if data and data.strip().startswith('filepath:'): + filepath = data.split('filepath:')[1].strip() + try: + with open(filepath, 'r') as file: + data = file.read() + except FileNotFoundError: + raise ValueError(f"File not found: {filepath}") + + if not data: + raise ValueError("Cannot add record type without definition. --data or --file is required.") + + try: + return json.loads(data) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON format: {e}") diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 2fee42fb..7d1cf747 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -43,6 +43,8 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('export', importer_commands.ExportCommand(), base.CommandScope.Vault) commands.register_command('breachwatch', breachwatch.BreachWatchCommand(), base.CommandScope.Vault, 'bw') commands.register_command('record-type-add', record_type.RecordTypeAddCommand(), base.CommandScope.Vault) + commands.register_command('record-type-edit', record_type.RecordTypeEditCommand(), base.CommandScope.Vault) + commands.register_command('record-type-delete', record_type.RecordTypeDeleteCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/vault/record_type_management.py b/keepersdk-package/src/keepersdk/vault/record_type_management.py index c019552c..55168629 100644 --- a/keepersdk-package/src/keepersdk/vault/record_type_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_type_management.py @@ -6,7 +6,7 @@ from ..proto import record_pb2 -def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str): +def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str, categories: List[str] = None): is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin if not is_enterprise_admin: raise ValueError('This command is restricted to Keeper Enterprise administrators.') @@ -26,7 +26,7 @@ def create_custom_record_type(vault: vault_online.VaultOnline, title: str, field record_type_data = { "$id": title, "description": description, - "categories": ["note"], + "categories": categories if categories else [], "fields": field_definitions } @@ -37,3 +37,72 @@ def create_custom_record_type(vault: vault_online.VaultOnline, title: str, field response = vault.keeper_auth.execute_auth_rest('vault/record_type_add', request_payload, response_type=record_pb2.RecordTypeModifyResponse) return response + + +def edit_custom_record_types(vault: vault_online.VaultOnline, record_type_id: int, title: str, fields: List[Dict[str, str]], description: str, categories: List[str] = None): + is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin + if not is_enterprise_admin: + raise ValueError('This command is restricted to Keeper Enterprise administrators.') + + if not fields: + raise ValueError('At least one field must be specified.') + + is_enterprise_rt, real_type_id = isEnterpriseRecordType(record_type_id) + + if not is_enterprise_rt: + raise ValueError('Only custom record types can be modified.') + + field_definitions = [] + for field in fields: + field_name = field.get("$ref") + if not field_name: + raise ValueError("Each field must contain a '$ref' key.") + if field_name not in record_types.FieldTypes and field_name not in record_types.RecordFields: + raise ValueError(f"Field '{field_name}' is not a valid RecordField.") + field_definitions.append({"$ref": field_name}) + + record_type_data = { + "$id": title, + "description": description, + "categories": categories if categories else [], + "fields": field_definitions + } + + request_payload = record_pb2.RecordType() + request_payload.content = json.dumps(record_type_data) + request_payload.scope = record_pb2.RT_ENTERPRISE + request_payload.recordTypeId = real_type_id + + response = vault.keeper_auth.execute_auth_rest('vault/record_type_update', request_payload, response_type=record_pb2.RecordTypeModifyResponse) + + return response + + +def delete_custom_record_types(vault: vault_online.VaultOnline, record_type_id: int): + is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin + if not is_enterprise_admin: + raise ValueError('This command is restricted to Keeper Enterprise administrators.') + + is_enterprise_rt, real_type_id = isEnterpriseRecordType(record_type_id) + + if not is_enterprise_rt: + raise ValueError('Only custom record types can be removed.') + + request_payload = record_pb2.RecordType() + request_payload.scope = record_pb2.RT_ENTERPRISE + request_payload.recordTypeId = real_type_id + + response = vault.keeper_auth.execute_auth_rest('vault/record_type_delete', request_payload, response_type=record_pb2.RecordTypeModifyResponse) + + return response + + +def isEnterpriseRecordType(record_type_id: int) -> bool: + num_rts_per_scope = 1_000_000 + enterprise_scope = record_pb2.RT_ENTERPRISE + min_id = num_rts_per_scope * enterprise_scope + max_id = min_id + num_rts_per_scope + is_enterprise_rt = min_id < record_type_id <= max_id + real_type_id = record_type_id % num_rts_per_scope + + return is_enterprise_rt, real_type_id \ No newline at end of file diff --git a/keepersdk-package/unit_tests/test_record_type_management.py b/keepersdk-package/unit_tests/test_record_type_management.py new file mode 100644 index 00000000..6f63e374 --- /dev/null +++ b/keepersdk-package/unit_tests/test_record_type_management.py @@ -0,0 +1,140 @@ +import unittest +from unittest.mock import MagicMock + +from keepersdk.proto import record_pb2 +from keepersdk.vault import record_type_management + + +class CreateCustomRecordTypeTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.vault.keeper_auth.execute_auth_rest = MagicMock() + self.vault.keeper_auth.auth_context.is_enterprise_admin = True + + def test_successful_creation(self): + title = "TestType" + fields = [{"$ref": "login"}] + description = "A test type" + categories = ["test", "example"] + record_type_management.record_types.FieldTypes = {"login": {}} + self.vault.keeper_auth.execute_auth_rest.return_value = record_pb2.RecordTypeModifyResponse() + result = record_type_management.create_custom_record_type(self.vault, title, fields, description, categories) + self.assertIsInstance(result, record_pb2.RecordTypeModifyResponse) + self.vault.keeper_auth.execute_auth_rest.assert_called_once_with( + 'vault/record_type_add', + unittest.mock.ANY, + response_type=record_pb2.RecordTypeModifyResponse + ) + + def test_not_enterprise_admin(self): + self.vault.keeper_auth.auth_context.is_enterprise_admin = False + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [{"$ref": "login"}], "desc", ["test"]) + self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) + + def test_missing_fields(self): + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [], "desc", ["test"]) + self.assertIn("At least one field", str(cm.exception)) + + def test_missing_ref(self): + record_type_management.record_types.FieldTypes = {"login": {}} + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [{}], "desc", ["test"]) + self.assertIn("Each field must contain a '$ref'", str(cm.exception)) + + def test_invalid_field_name(self): + record_type_management.record_types.FieldTypes = {"login": {}} + with self.assertRaises(ValueError) as cm: + record_type_management.create_custom_record_type(self.vault, "Title", [{"$ref": "not_a_field"}], "desc", ["test"]) + self.assertIn("is not a valid RecordField", str(cm.exception)) + + +class EditCustomRecordTypesTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.vault.keeper_auth.execute_auth_rest = MagicMock() + self.vault.keeper_auth.auth_context.is_enterprise_admin = True + record_type_management.record_types.FieldTypes = {"login": {}} + + def test_successful_edit(self): + title = "EditedType" + fields = [{"$ref": "login"}] + description = "Edited description" + categories = ["test", "example"] + record_type_id = 2000001 + self.vault.keeper_auth.execute_auth_rest.return_value = record_pb2.RecordTypeModifyResponse() + result = record_type_management.edit_custom_record_types(self.vault, record_type_id, title, fields, description, categories) + self.assertIsInstance(result, record_pb2.RecordTypeModifyResponse) + self.vault.keeper_auth.execute_auth_rest.assert_called_once_with( + 'vault/record_type_update', + unittest.mock.ANY, + response_type=record_pb2.RecordTypeModifyResponse + ) + + def test_not_enterprise_admin(self): + self.vault.keeper_auth.auth_context.is_enterprise_admin = False + record_type_id = 2000001 + with self.assertRaises(ValueError) as cm: + record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [{"$ref": "login"}], "desc", ["test"]) + self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) + + def test_not_enterprise_record_type_id(self): + record_type_id = 1 + with self.assertRaises(ValueError) as cm: + record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [{"$ref": "login"}], "desc", ["test"]) + self.assertIn("can be modified", str(cm.exception)) + + def test_missing_fields(self): + record_type_id = 2000001 + with self.assertRaises(ValueError) as cm: + record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [], "desc", ["test"]) + self.assertIn("At least one field", str(cm.exception)) + + def test_missing_ref(self): + record_type_id = 2000001 + with self.assertRaises(ValueError) as cm: + record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [{}], "desc", ["test"]) + self.assertIn("Each field must contain a '$ref'", str(cm.exception)) + + def test_invalid_field_name(self): + record_type_id = 2000001 + record_type_management.record_types.FieldTypes = {"login": {}} + with self.assertRaises(ValueError) as cm: + record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [{"$ref": "not_a_field"}], "desc", ["test"]) + self.assertIn("is not a valid RecordField", str(cm.exception)) + + +class DeleteCustomRecordTypesTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.vault.keeper_auth.execute_auth_rest = MagicMock() + self.vault.keeper_auth.auth_context.is_enterprise_admin = True + + def test_successful_delete(self): + record_type_id = 2000001 + self.vault.keeper_auth.execute_auth_rest.return_value = record_pb2.RecordTypeModifyResponse() + result = record_type_management.delete_custom_record_types(self.vault, record_type_id) + self.assertIsInstance(result, record_pb2.RecordTypeModifyResponse) + self.vault.keeper_auth.execute_auth_rest.assert_called_once_with( + 'vault/record_type_delete', + unittest.mock.ANY, + response_type=record_pb2.RecordTypeModifyResponse + ) + + def test_not_enterprise_admin(self): + self.vault.keeper_auth.auth_context.is_enterprise_admin = False + record_type_id = 2000001 + with self.assertRaises(ValueError) as cm: + record_type_management.delete_custom_record_types(self.vault, record_type_id) + self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) + + def test_not_enterprise_record_type_id(self): + record_type_id = 1 + with self.assertRaises(ValueError) as cm: + record_type_management.delete_custom_record_types(self.vault, record_type_id) + self.assertIn("can be removed", str(cm.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/keepersdk-package/unit_tests/test_sync_down.py b/keepersdk-package/unit_tests/test_sync_down.py index f271f0a9..4ac8a20d 100644 --- a/keepersdk-package/unit_tests/test_sync_down.py +++ b/keepersdk-package/unit_tests/test_sync_down.py @@ -129,49 +129,5 @@ def test_delete_shared_folder(self): self.assertIsNone(vault.vault_data.get_shared_folder(shared_folder_uid)) -class CreateCustomRecordTypeTestCase(unittest.TestCase): - def setUp(self): - self.vault = MagicMock() - self.vault.keeper_auth.execute_auth_rest = MagicMock() - self.vault.keeper_auth.auth_context.is_enterprise_admin = True - - def test_successful_creation(self): - title = "TestType" - fields = [{"$ref": "login"}] - description = "A test type" - record_type_management.record_types.FieldTypes = {"login": {}} - self.vault.keeper_auth.execute_auth_rest.return_value = record_pb2.RecordTypeModifyResponse() - result = record_type_management.create_custom_record_type(self.vault, title, fields, description) - self.assertIsInstance(result, record_pb2.RecordTypeModifyResponse) - self.vault.keeper_auth.execute_auth_rest.assert_called_once_with( - 'vault/record_type_add', - unittest.mock.ANY, - response_type=record_pb2.RecordTypeModifyResponse - ) - - def test_not_enterprise_admin(self): - self.vault.keeper_auth.auth_context.is_enterprise_admin = False - with self.assertRaises(ValueError) as cm: - record_type_management.create_custom_record_type(self.vault, "Title", [{"$ref": "login"}], "desc") - self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) - - def test_missing_fields(self): - with self.assertRaises(ValueError) as cm: - record_type_management.create_custom_record_type(self.vault, "Title", [], "desc") - self.assertIn("At least one field", str(cm.exception)) - - def test_missing_ref(self): - record_type_management.record_types.FieldTypes = {"login": {}} - with self.assertRaises(ValueError) as cm: - record_type_management.create_custom_record_type(self.vault, "Title", [{}], "desc") - self.assertIn("Each field must contain a '$ref'", str(cm.exception)) - - def test_invalid_field_name(self): - record_type_management.record_types.FieldTypes = {"login": {}} - with self.assertRaises(ValueError) as cm: - record_type_management.create_custom_record_type(self.vault, "Title", [{"$ref": "not_a_field"}], "desc") - self.assertIn("is not a valid RecordField", str(cm.exception)) - - if __name__ == "__main__": unittest.main() From d1eaa2be3a753e0bc1986c06dc84e5d1cb8aca91 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 10 Jun 2025 10:32:05 +0530 Subject: [PATCH 05/44] record_type_info and load_record_types functions added (#12) * record_type_info and load_record_types functions added * Moved functions to cli commands * Corrected review changes --- .../src/keepercli/commands/record_type.py | 196 +++++++++++++++++- .../keepercli/commands/record_type_utils.py | 148 +++++++++++++ .../src/keepercli/register_commands.py | 2 + .../keepersdk/vault/record_type_management.py | 26 +-- .../unit_tests/test_record_type_management.py | 14 +- 5 files changed, 349 insertions(+), 37 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/record_type_utils.py diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py index 643da9c6..4151f471 100644 --- a/keepercli-package/src/keepercli/commands/record_type.py +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -2,11 +2,12 @@ import json import logging -from keepersdk.vault import record_type_management +from keepersdk.vault import record_type_management, record_types -from . import base +from . import base, record_type_utils from ..params import KeeperParams from .. import api +from ..helpers import report_utils logger = api.get_logger() @@ -45,6 +46,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: context.vault, title, fields, description, categories ) logger.info(f"Custom record type '{title}' created successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") + return class RecordTypeEditCommand(base.ArgparseCommand): @@ -92,6 +94,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: context.vault, record_type_id, title, fields, description, categories ) logger.info(f"Custom record type (ID: {record_type_id}) updated successfully with fields: {[f['$ref'] for f in fields]} and recordTypeId: {result.recordTypeId}") + return class RecordTypeDeleteCommand(base.ArgparseCommand): @@ -118,6 +121,195 @@ def execute(self, context: KeeperParams, **kwargs) -> None: result = record_type_management.delete_custom_record_types(context.vault, record_type_id) logger.info(f"Custom record type deleted successfully with record type id: {result.recordTypeId}") + return + + +class RecordTypeInfoCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='record-type-info', + description='Get record type info' + ) + RecordTypeInfoCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '-lr', + '--list-record-type', + type=str, + dest='record_name', + action='store', + default=None, + const = '*', + nargs='?', + help='list record type by name or use * to list all' + ) + parser.add_argument( + '-lf', + '--list-field', + type=str, + dest='field_name', + action='store', + default=None, + help='list field type by name or use * to list all' + ) + parser.add_argument( + '-e', + '--example', + dest='example', + action='store_true', + help='Use --example to generate example JSON' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + vault = context.vault + example = kwargs.get('example', False) + field_name = kwargs.get('field_name') + record_type_name = kwargs.get('record_name') + + if field_name is not None: + headers = ('Field Type ID', 'Lookup', 'Multiple', 'Description') + show_all_fields = field_name.strip() == '' or field_name.strip() == '*' + if show_all_fields: + rows = [] + for ft in record_types.FieldTypes.values(): + rows.append(record_type_utils.get_field_definitions(ft)) + return report_utils.dump_report_data(rows, headers, column_width='auto', fmt='simple') + else: + # Fetch a specific field type + ft = record_types.FieldTypes.get(field_name) + if not ft: + raise ValueError(f"Field type '{field_name}' is not a valid RecordField.") + row = record_type_utils.get_field_definitions(ft) + return report_utils.dump_report_data([row], headers, column_width='auto', fmt='simple') + + if record_type_name and record_type_name != '*' and record_type_name != '' and example: + record_type_example = record_type_utils.get_record_type_example(vault, record_type_name) + logger.info(record_type_example) + return + + # Record Types + if record_type_name and record_type_name != '*' and record_type_name != '': + #Fetch a specific record type + record_type = vault.vault_data.get_record_type_by_name(record_type_name) + if not record_type: + raise ValueError(f"Record type '{record_type_name}' not found.") + + rows = [] + fields = record_type.fields + scope = record_type_utils.get_record_type_scope(record_type.scope) + rows.append([ + record_type.id, + record_type.name, + scope, + fields[0].label if hasattr(fields[0], 'label') else str(fields[0]) + ]) + for field in fields[1:]: + rows.append(['', '', '', field.label if hasattr(field, 'label') else str(field)]) + + headers = ('id', 'name', 'scope', 'fields') + return report_utils.dump_report_data(rows, headers, column_width='auto', fmt='simple') + else: + #Show all record types + record_types_list = record_type_utils.get_record_types(vault) + if not record_types_list: + raise ValueError("No record types found.") + + rows = [] + for rtid, name, scope in record_types_list: + rows.append([rtid, name, scope]) + + headers = ('Record Type ID', 'Record Type Name', 'Record Type Scope') + return report_utils.dump_report_data(rows, headers, column_width='auto', fmt='simple') + + +class LoadRecordTypesCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='load-record-types', + description='Loads custom record types from a JSON file.' + ) + parser.add_argument( + '--file', + dest='file', + action='store', + required=True, + help='Path to the JSON file containing the record type definition.' + ) + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + filepath = kwargs.get('file') + if not filepath: + raise ValueError("Missing required argument: --file") + + count = 0 + record_types_list = record_type_utils.validate_record_type_file(filepath) + + loaded_record_types = set() + existing_record_types = record_type_utils.get_record_types(context.vault) + if existing_record_types: + for existing_record_type in existing_record_types: + loaded_record_types.add(existing_record_type[1].lower()) + + for record_type in record_types_list: + record_type_name = record_type.get('record_type_name') + if not record_type_name: + logger.error('Record type name is missing in the record type definition.', record_type) + continue + + record_type_name = record_type_name[:30] + if record_type_name.lower() in loaded_record_types: + logger.info(f'Record type "{record_type_name}" already exists. Skipping.') + continue + + fields = record_type.get('fields') + if not isinstance(fields, list): + logger.error('Fields must be a list in the record type definition.', record_type) + continue + + is_valid = True + add_fields = [] + for field in fields: + field_type = field.get('$type') + if field_type not in record_types.RecordFields: + is_valid = False + break + fo = {'$ref': field.get('$type')} + if field.get('required') is True: + fo['required'] = True + add_fields.append(fo) + if not is_valid: + logger.error('Invalid field type in the record type definition.', record_type) + continue + + if len(add_fields) == 0: + logger.error('No fields found in the record type definition.', record_type) + continue + + record_type_management.create_custom_record_type( + vault=context.vault, + title=record_type_name, + fields=add_fields, + description=record_type.get('description') or '', + categories=record_type.get('categories') or [] + ) + count += 1 + + if count != 0: + logger.info(f"Custom record types imported successfully. {count} record types were added.") + else: + logger.info("No custom record types were imported. Record types already exist in the vault or the file is empty.") + return record_implicit_fields = { diff --git a/keepercli-package/src/keepercli/commands/record_type_utils.py b/keepercli-package/src/keepercli/commands/record_type_utils.py new file mode 100644 index 00000000..204f97ac --- /dev/null +++ b/keepercli-package/src/keepercli/commands/record_type_utils.py @@ -0,0 +1,148 @@ +import json + +from keepersdk.vault import vault_online, storage_types, record_types, vault_types +from keepersdk.proto import record_pb2 + +def get_record_type_example(vault: vault_online.VaultOnline, record_type_name: str) -> str: + STR_VALUE = 'text' + + result = '' + rte = {} + record_type = vault.vault_data.get_record_type_by_name(record_type_name) + if record_type: + record_type_fields = record_type.fields + rte = { + 'type': record_type_name, + 'title': STR_VALUE, + 'notes': STR_VALUE, + 'fields': [], + 'custom': [] + } + + fields = record_type.fields or [] + fields = [x.label for x in fields] + for fname in fields: + ft = get_field_type(fname) + + required = next((x.required for x in record_type_fields if x.label == fname), None) + label = next((x.label for x in record_type_fields if x.label == fname), None) + + val = { + 'type': fname, + 'value': [ft.get('value') or ''], + 'required': required, + 'label': label + } + + if fname not in ('fileRef', 'addressRef', 'cardRef'): + if fname == 'phone' and ft and 'sample' in ft and 'region' in ft['sample']: + ft['sample']['region'] = 'US' + + rte['fields'].append(val) + else: + raise ValueError(f'No record type found with name {record_type_name}. Use "record-type-info" to list all record types') + + result = json.dumps(rte, indent=2) if rte else '' + return result + + +def get_record_types(vault:vault_online.VaultOnline) -> list[vault_types.RecordType]: + records = [] # (recordTypeId, name, scope) + record_types = vault.vault_data.get_record_types() + + if record_types: + for record_type in record_types: + name = record_type.name + scope = get_record_type_scope(record_type.scope) + records.append((record_type.id, name, scope)) + + return records + + +def get_field_type(id): + ftypes = [ + {**vars(record_types.RecordFields[rkey]), **vars(record_types.FieldTypes[fkey])} + for rkey in record_types.RecordFields + for fkey in record_types.FieldTypes + if record_types.RecordFields[rkey].type == record_types.FieldTypes[fkey].name + ] + result = next((ft for ft in ftypes if id.lower() == ft.get('name').lower()), {}) + if result: + # Determine value based on whether the id matches a FieldType or RecordField + field_type_obj = next((ft for ft in record_types.FieldTypes.values() if ft.name.lower() == id.lower()), None) + + if field_type_obj: + value = getattr(field_type_obj, 'value', None) + else: + value = result.get('type', None) + + result = { + 'id': result.get('$id') or result.get('name') or '', + 'type': result.get('type') or result.get('name') or '', + 'value': value, + } + return result + + +def isEnterpriseRecordType(record_type_id: int) -> bool: + num_rts_per_scope = 1_000_000 + enterprise_scope = record_pb2.RT_ENTERPRISE + min_id = num_rts_per_scope * enterprise_scope + max_id = min_id + num_rts_per_scope + is_enterprise_rt = min_id < record_type_id <= max_id + real_type_id = record_type_id % num_rts_per_scope + + return is_enterprise_rt, real_type_id + + +def get_field_definitions(field: record_types.FieldType): + recordfield_names = {rf.name for rf in record_types.RecordFields.values()} + lookup = field.name if field.name in recordfield_names else "" + multiple = ( + record_types.RecordFields[field.name].multiple.name + if lookup else "Optional" + ) + row = [ + field.name, + lookup, + multiple, + field.description + ] + return row + + +scope_map = { + storage_types.RecordTypeScope.Standard: 'Standard', + storage_types.RecordTypeScope.User: 'User', + storage_types.RecordTypeScope.Enterprise: 'Enterprise' +} + + +def get_record_type_scope(scope: storage_types.RecordTypeScope) -> str: + return scope_map.get(scope, str(scope)) + + +def validate_record_type_file(file_path: str) -> list: + if not file_path: + raise ValueError('File path is required.') + + if not file_path.endswith('.json'): + raise ValueError('Record type file must be a JSON file.') + + try: + with open(file_path, 'r') as f: + json_obj = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f'Invalid JSON in record type file: {e}') + except FileNotFoundError: + raise ValueError(f'Record type file not found: {file_path}') + + if not isinstance(json_obj, dict): + raise ValueError('Invalid custom record types file') + + record_types_list = json_obj.get('record_types') + + if not isinstance(record_types_list, list): + raise ValueError('Invalid custom record types list') + + return record_types_list \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 7d1cf747..04dff18b 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -45,6 +45,8 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('record-type-add', record_type.RecordTypeAddCommand(), base.CommandScope.Vault) commands.register_command('record-type-edit', record_type.RecordTypeEditCommand(), base.CommandScope.Vault) commands.register_command('record-type-delete', record_type.RecordTypeDeleteCommand(), base.CommandScope.Vault) + commands.register_command('record-type-info', record_type.RecordTypeInfoCommand(), base.CommandScope.Vault, 'rti') + commands.register_command('load-record-types', record_type.LoadRecordTypesCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/vault/record_type_management.py b/keepersdk-package/src/keepersdk/vault/record_type_management.py index 55168629..3e103a72 100644 --- a/keepersdk-package/src/keepersdk/vault/record_type_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_type_management.py @@ -4,7 +4,9 @@ from . import vault_online, record_types from ..proto import record_pb2 +from ..utils import get_logger +logger = get_logger() def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str, categories: List[str] = None): is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin @@ -47,11 +49,6 @@ def edit_custom_record_types(vault: vault_online.VaultOnline, record_type_id: in if not fields: raise ValueError('At least one field must be specified.') - is_enterprise_rt, real_type_id = isEnterpriseRecordType(record_type_id) - - if not is_enterprise_rt: - raise ValueError('Only custom record types can be modified.') - field_definitions = [] for field in fields: field_name = field.get("$ref") @@ -71,7 +68,7 @@ def edit_custom_record_types(vault: vault_online.VaultOnline, record_type_id: in request_payload = record_pb2.RecordType() request_payload.content = json.dumps(record_type_data) request_payload.scope = record_pb2.RT_ENTERPRISE - request_payload.recordTypeId = real_type_id + request_payload.recordTypeId = record_type_id response = vault.keeper_auth.execute_auth_rest('vault/record_type_update', request_payload, response_type=record_pb2.RecordTypeModifyResponse) @@ -82,27 +79,12 @@ def delete_custom_record_types(vault: vault_online.VaultOnline, record_type_id: is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin if not is_enterprise_admin: raise ValueError('This command is restricted to Keeper Enterprise administrators.') - - is_enterprise_rt, real_type_id = isEnterpriseRecordType(record_type_id) - - if not is_enterprise_rt: - raise ValueError('Only custom record types can be removed.') request_payload = record_pb2.RecordType() request_payload.scope = record_pb2.RT_ENTERPRISE - request_payload.recordTypeId = real_type_id + request_payload.recordTypeId = record_type_id response = vault.keeper_auth.execute_auth_rest('vault/record_type_delete', request_payload, response_type=record_pb2.RecordTypeModifyResponse) return response - -def isEnterpriseRecordType(record_type_id: int) -> bool: - num_rts_per_scope = 1_000_000 - enterprise_scope = record_pb2.RT_ENTERPRISE - min_id = num_rts_per_scope * enterprise_scope - max_id = min_id + num_rts_per_scope - is_enterprise_rt = min_id < record_type_id <= max_id - real_type_id = record_type_id % num_rts_per_scope - - return is_enterprise_rt, real_type_id \ No newline at end of file diff --git a/keepersdk-package/unit_tests/test_record_type_management.py b/keepersdk-package/unit_tests/test_record_type_management.py index 6f63e374..b250b31c 100644 --- a/keepersdk-package/unit_tests/test_record_type_management.py +++ b/keepersdk-package/unit_tests/test_record_type_management.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from keepersdk.proto import record_pb2 from keepersdk.vault import record_type_management @@ -79,12 +79,6 @@ def test_not_enterprise_admin(self): record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [{"$ref": "login"}], "desc", ["test"]) self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) - def test_not_enterprise_record_type_id(self): - record_type_id = 1 - with self.assertRaises(ValueError) as cm: - record_type_management.edit_custom_record_types(self.vault, record_type_id, "Title", [{"$ref": "login"}], "desc", ["test"]) - self.assertIn("can be modified", str(cm.exception)) - def test_missing_fields(self): record_type_id = 2000001 with self.assertRaises(ValueError) as cm: @@ -129,12 +123,6 @@ def test_not_enterprise_admin(self): record_type_management.delete_custom_record_types(self.vault, record_type_id) self.assertIn("restricted to Keeper Enterprise administrators", str(cm.exception)) - def test_not_enterprise_record_type_id(self): - record_type_id = 1 - with self.assertRaises(ValueError) as cm: - record_type_management.delete_custom_record_types(self.vault, record_type_id) - self.assertIn("can be removed", str(cm.exception)) - if __name__ == "__main__": unittest.main() From 39738e63de28d81ce2507c2b4a470aea545c155c Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 10 Jun 2025 16:35:42 +0530 Subject: [PATCH 06/44] download-record-types command added --- .../src/keepercli/commands/record_type.py | 130 ++++++++++++------ .../keepercli/commands/record_type_utils.py | 54 ++++++++ .../src/keepercli/register_commands.py | 1 + .../src/keepersdk/importer/keeper_format.py | 2 +- 4 files changed, 141 insertions(+), 46 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py index 4151f471..37837f49 100644 --- a/keepercli-package/src/keepercli/commands/record_type.py +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -1,8 +1,9 @@ import argparse import json -import logging +import os from keepersdk.vault import record_type_management, record_types +from keepersdk.importer import keeper_format, import_data from . import base, record_type_utils from ..params import KeeperParams @@ -33,9 +34,9 @@ def execute(self, context: KeeperParams, **kwargs) -> None: data = kwargs.get('data') - record_type = load_data(data) + record_type = record_type_utils.load_data(data) - is_valid_data(record_type) + record_type_utils.is_valid_data(record_type) title = record_type.get('$id') fields = record_type.get('fields') @@ -81,9 +82,9 @@ def execute(self, context: KeeperParams, **kwargs) -> None: if not record_type_id: raise ValueError("Missing required argument: record_type_id") - record_type = load_data(data) + record_type = record_type_utils.load_data(data) - is_valid_data(record_type) + record_type_utils.is_valid_data(record_type) title = record_type.get('$id') fields = record_type.get('fields') @@ -312,49 +313,88 @@ def execute(self, context: KeeperParams, **kwargs) -> None: return -record_implicit_fields = { - 'title': '', # string - 'custom': [], # Array of Field Data objects - 'notes': '' # string -} +class DownloadRecordTypesCommand(base.ArgparseCommand): + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='download-record-types', + description='Download custom record types to a JSON file.' + ) + DownloadRecordTypesCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) -def is_valid_data(record_type): - title = record_type.get('$id') - fields = record_type.get('fields') - - if not title: - raise ValueError("Record type must have a '$id' field.") - if not fields or not isinstance(fields, list): - raise ValueError("Record type must include a list of 'fields'.") - - # Implicit fields - always present on any record, no need to be specified in the template: title, custom, notes - implicit_field_names = [x for x in record_implicit_fields] - implicit_fields = [r for r in record_type if r in implicit_field_names] - if implicit_fields: - error = {'error: Implicit fields not allowed in record type definition: ' + str(implicit_fields)} - raise ValueError(error) - - rt_attributes = ('$id', 'categories', 'description', 'fields') - bad_attributes = [r for r in record_type if r not in rt_attributes and r not in implicit_field_names] - if bad_attributes: - logging.debug(f'Unknown attributes in record type definition: {bad_attributes}') + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '--name', + dest='name', + action='store', + type=str, + help='Output file name. "record_types.json" if omitted.' + ) + parser.add_argument( + '--ssh-key-file', + dest='ssh-key-file', + action="store_true", + help='Prefer store SSH keys as file attachments rather than fields on a record' + ) + parser.add_argument( + '--source', + dest='source', + required=True, + choices=['keeper'], + help='Record Type Source. Only "keeper" is currently supported.' + ) + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") -def load_data(data): + file_name = kwargs.get('name') or 'record_types.json' + source = kwargs.get('source') + ssh_key_file = kwargs.get('ssh-key-file') - if data and data.strip().startswith('filepath:'): - filepath = data.split('filepath:')[1].strip() - try: - with open(filepath, 'r') as file: - data = file.read() - except FileNotFoundError: - raise ValueError(f"File not found: {filepath}") + if source == 'keeper': + plugin = keeper_format.KeeperRecordTypeDownload(vault=context.vault) + #elif to be added for any other methods (currently only keeper is implemented) - if not data: - raise ValueError("Cannot add record type without definition. --data or --file is required.") - - try: - return json.loads(data) - except json.JSONDecodeError as e: - raise ValueError(f"Invalid JSON format: {e}") + record_types = [] + for rt in plugin.download_record_type(): + if not isinstance(rt, import_data.RecordType): + continue + need_file_ref = False + rto = { + 'record_type_name': rt.name, + 'fields': [] + } + if rt.description: + rto['description'] = rt.description + + for f in rt.fields: + if ssh_key_file is True and f.type == 'keyPair': + need_file_ref = True + continue + fo = {'$type': f.type} + if f.label: + fo['label'] = f.label + if f.required is True: + fo['required'] = True + rto['fields'].append(fo) + + if need_file_ref: + has_ref = next((True for x in rto['fields'] if x['$type'] == 'fileRef'), False) + if not has_ref: + rto['fields'].append({'$type': 'fileRef'}) + record_types.append(rto) + + if len(record_types) > 0: + output = { + 'record_types': record_types + } + try: + with open(file_name, 'wt', encoding='utf-8') as file: + json.dump(output, file, indent=2) + logger.info('Downloaded %d record types to "%s"', len(record_types), os.path.abspath(file_name)) + except Exception as e: + logger.error('Failed to write record types to file "%s": %s', file_name, str(e)) + else: + logger.info('No record types are downloaded') diff --git a/keepercli-package/src/keepercli/commands/record_type_utils.py b/keepercli-package/src/keepercli/commands/record_type_utils.py index 204f97ac..4871df09 100644 --- a/keepercli-package/src/keepercli/commands/record_type_utils.py +++ b/keepercli-package/src/keepercli/commands/record_type_utils.py @@ -1,8 +1,62 @@ import json +from .. import api + from keepersdk.vault import vault_online, storage_types, record_types, vault_types from keepersdk.proto import record_pb2 + +record_implicit_fields = { + 'title': '', # string + 'custom': [], # Array of Field Data objects + 'notes': '' # string +} + + +logger = api.get_logger() + + +def is_valid_data(record_type): + title = record_type.get('$id') + fields = record_type.get('fields') + + if not title: + raise ValueError("Record type must have a '$id' field.") + if not fields or not isinstance(fields, list): + raise ValueError("Record type must include a list of 'fields'.") + + # Implicit fields - always present on any record, no need to be specified in the template: title, custom, notes + implicit_field_names = [x for x in record_implicit_fields] + implicit_fields = [r for r in record_type if r in implicit_field_names] + if implicit_fields: + error = {'error: Implicit fields not allowed in record type definition: ' + str(implicit_fields)} + raise ValueError(error) + + rt_attributes = ('$id', 'categories', 'description', 'fields') + bad_attributes = [r for r in record_type if r not in rt_attributes and r not in implicit_field_names] + if bad_attributes: + logger.debug(f'Unknown attributes in record type definition: {bad_attributes}') + + +def load_data(data): + + if data and data.strip().startswith('filepath:'): + filepath = data.split('filepath:')[1].strip() + try: + with open(filepath, 'r') as file: + data = file.read() + except FileNotFoundError: + raise ValueError(f"File not found: {filepath}") + + if not data: + raise ValueError("Cannot add record type without definition. --data or --file is required.") + + try: + return json.loads(data) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON format: {e}") + + def get_record_type_example(vault: vault_online.VaultOnline, record_type_name: str) -> str: STR_VALUE = 'text' diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 04dff18b..b8750905 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -47,6 +47,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('record-type-delete', record_type.RecordTypeDeleteCommand(), base.CommandScope.Vault) commands.register_command('record-type-info', record_type.RecordTypeInfoCommand(), base.CommandScope.Vault, 'rti') commands.register_command('load-record-types', record_type.LoadRecordTypesCommand(), base.CommandScope.Vault) + commands.register_command('download-record-types', record_type.DownloadRecordTypesCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/importer/keeper_format.py b/keepersdk-package/src/keepersdk/importer/keeper_format.py index c9111ee6..0411a1ec 100644 --- a/keepersdk-package/src/keepersdk/importer/keeper_format.py +++ b/keepersdk-package/src/keepersdk/importer/keeper_format.py @@ -497,7 +497,7 @@ def __init__(self, vault: vault_online.VaultOnline) -> None: def download_record_type(self, **kwargs) -> Iterable[import_data.RecordType]: for ert in self.vault.vault_data.get_record_types(): - if ert.scope == vault_types.RecordTypeScope.Enterprise: + if ert.scope != vault_types.RecordTypeScope.Enterprise: continue rt = import_data.RecordType() rt.name = ert.name From e570e17ecbdaad3b7d64198066e33f5305f009f5 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 18 Jun 2025 17:44:03 +0530 Subject: [PATCH 07/44] secrets-manager-app list and get functions and commands --- .../src/keepercli/commands/secrets_manager.py | 94 ++++++++ .../src/keepercli/helpers/ksm_utils.py | 28 +++ .../src/keepercli/register_commands.py | 5 +- keepersdk-package/src/keepersdk/vault/ksm.py | 35 +++ .../src/keepersdk/vault/ksm_management.py | 130 ++++++++++++ .../unit_tests/test_ksm_management.py | 200 ++++++++++++++++++ 6 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 keepercli-package/src/keepercli/commands/secrets_manager.py create mode 100644 keepercli-package/src/keepercli/helpers/ksm_utils.py create mode 100644 keepersdk-package/src/keepersdk/vault/ksm.py create mode 100644 keepersdk-package/src/keepersdk/vault/ksm_management.py create mode 100644 keepersdk-package/unit_tests/test_ksm_management.py diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py new file mode 100644 index 00000000..21ff64a4 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -0,0 +1,94 @@ +import argparse + +from keepersdk.vault import ksm_management + +from . import base +from .. import api +from ..params import KeeperParams +from ..helpers import report_utils, ksm_utils + + +logger = api.get_logger() + + +class SecretsManagerAppCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='secrets-manager app', + description='Keeper Secrets Manager (KSM) Commands', + ) + SecretsManagerAppCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + def add_arguments_to_parser(parser: argparse.ArgumentParser): + + parser.add_argument( + '--command', type=str, action='store', required=True, dest='command', + help='One of: "list", "get", "create", "remove", "share" or "unshare"' + ) + parser.add_argument( + '--name', '-n', type=str, dest='name', action='store', required=False, help='Application Name or UID' + ) + parser.add_argument( + '--purge', '-p', action='store_true', help='remove the record from all folders and purge it from the trash' + ) + parser.add_argument( + '-f', '--force', dest='force', action='store_true', help='Force add or remove app' + ) + parser.add_argument( + '--email', action='store', type=str, dest='email', help='Email of user to grant / remove application access to / from' + ) + parser.add_argument( + '--admin', action='store_true', help='Allow share recipient to manage application' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + command = kwargs.get('command') + + if command == 'list': + return self.list_app(context.vault) + + elif command == 'get': + app_name = kwargs.get('name') + return self.get_app(context.vault, app_name) + + else: + logger.error(f"Unknown command '{command}'. Available commands: list, get, create, remove, share and unshare.") + return + + + def list_app(self, vault): + app_list = ksm_management.list_secrets_manager_apps(vault) + headers = ['App name', 'App UID', 'Records', 'Folders', 'Devices', 'Last Access'] + rows = [ + [app.name, app.uid, app.records, app.folders, app.count, app.last_access] + for app in app_list + ] + report_utils.dump_report_data(rows, headers=headers, fmt='table') + + + def get_app(self, vault, app_name): + if not app_name: + logger.error("Application name or UID is required for 'app get'. Use --name='example' to set it.") + return + + app = ksm_management.get_secrets_manager_app(vault=vault, uid_or_name=app_name) + + logger.info(f'\nSecrets Manager Application\n' + f'App Name: {app.name}\n' + f'App UID: {app.uid}') + + if len(app.client_devices) > 0: + ksm_utils.print_client_device_info(app.client_devices) + else: + logger.info('\nNo client devices registered for this Application\n') + + if app.shared_secrets: + ksm_utils.print_shared_secrets_info(app.shared_secrets) + else: + logger.info('\tThere are no shared secrets to this application') + return \ No newline at end of file diff --git a/keepercli-package/src/keepercli/helpers/ksm_utils.py b/keepercli-package/src/keepercli/helpers/ksm_utils.py new file mode 100644 index 00000000..68945a93 --- /dev/null +++ b/keepercli-package/src/keepercli/helpers/ksm_utils.py @@ -0,0 +1,28 @@ +from keepersdk.vault import ksm + +from .. import api +from ..helpers import report_utils + +logger = api.get_logger() + +def print_client_device_info(client_devices: list[ksm.ClientDevice]) -> None: + for index, client_device in enumerate(client_devices, start=1): + client_devices_str = f"\nClient Device {index}\n" \ + f"=============================\n" \ + f' Device Name: {client_device.name}\n' \ + f' Short ID: {client_device.short_id}\n' \ + f' Created On: {client_device.created_on}\n' \ + f' Expires On: {client_device.expires_on}\n' \ + f' First Access: {client_device.first_access}\n' \ + f' Last Access: {client_device.last_access}\n' \ + f' IP Lock: {client_device.ip_lock}\n' \ + f' IP Address: {client_device.ip_address or "--"}' + logger.info(client_devices_str) + +def print_shared_secrets_info(shared_secrets: list[ksm.SharedSecretsInfo]) -> None: + shares_table_fields = ['Share Type', 'UID', 'Title', 'Permissions'] + rows = [ + [secrets.type, secrets.uid, secrets.name, secrets.permissions] + for secrets in shared_secrets + ] + report_utils.dump_report_data(rows, shares_table_fields, fmt='table') \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index b8750905..1d227999 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -23,7 +23,9 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): - from .commands import vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, record_type + from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, + record_type, secrets_manager) + commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) commands.register_command('ls', vault_folder.FolderListCommand(), base.CommandScope.Vault) @@ -48,6 +50,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('record-type-info', record_type.RecordTypeInfoCommand(), base.CommandScope.Vault, 'rti') commands.register_command('load-record-types', record_type.LoadRecordTypesCommand(), base.CommandScope.Vault) commands.register_command('download-record-types', record_type.DownloadRecordTypesCommand(), base.CommandScope.Vault) + commands.register_command('secrets-manager-app', secrets_manager.SecretsManagerAppCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/vault/ksm.py b/keepersdk-package/src/keepersdk/vault/ksm.py new file mode 100644 index 00000000..be9a7633 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/ksm.py @@ -0,0 +1,35 @@ +import datetime +from dataclasses import dataclass +from typing import Optional + + +@dataclass(frozen=True) +class ClientDevice: + name: str + short_id: str + created_on: datetime + expires_on: datetime + first_access: datetime + last_access: datetime + ip_lock: bool + ip_address: str + + +@dataclass(frozen=True) +class SharedSecretsInfo: + type: str + uid: str + name: str + permissions: str + + +@dataclass(frozen=True) +class SecretsManagerApp: + name: str + uid: str + records: int + folders: int + count: int + last_access: datetime + client_devices: Optional[list[ClientDevice]] = None + shared_secrets: Optional[list[SharedSecretsInfo]] = None diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py new file mode 100644 index 00000000..dd46a95d --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -0,0 +1,130 @@ +import datetime +import time + +from . import vault_online, ksm +from ..proto import APIRequest_pb2 +from ..proto.APIRequest_pb2 import GetApplicationsSummaryResponse, ApplicationShareType, GetAppInfoRequest, GetAppInfoResponse +from ..proto.enterprise_pb2 import GENERAL +from .. import utils + +URL_GET_SUMMARY_API = 'vault/get_applications_summary' +URL_GET_APP_INFO_API = 'vault/get_app_info' +CLIENT_SHORT_ID_LENGTH = 8 + + +def list_secrets_manager_apps(vault: vault_online.VaultOnline) -> list[ksm.SecretsManagerApp]: + response = vault.keeper_auth.execute_auth_rest( + URL_GET_SUMMARY_API, + request=None, + response_type=GetApplicationsSummaryResponse + ) + + apps_list = [] + for app_summary in response.applicationSummary: + uid = utils.base64_url_encode(app_summary.appRecordUid) + app_record = vault.vault_data.load_record(uid) + name = getattr(app_record, 'title', '') if app_record else '' + last_access = int_to_datetime(app_summary.lastAccess) + secrets_app = ksm.SecretsManagerApp( + name=name, + uid=uid, + records=app_summary.folderRecords, + folders=app_summary.folderShares, + count=app_summary.clientCount, + last_access=last_access + ) + apps_list.append(secrets_app) + + return apps_list + + +def get_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str) -> ksm.SecretsManagerApp: + ksm_app = next((r for r in vault.vault_data.records() if r.record_uid == uid_or_name or r.title == uid_or_name), None) + if not ksm_app: + raise ValueError(f'No application found with UID/Name: {uid_or_name}') + + app_infos = get_app_info(vault=vault, app_uid=ksm_app.record_uid) + if not app_infos: + raise ValueError('No Secrets Manager Applications returned.') + + app_info = app_infos[0] + client_devices = [x for x in app_info.clients if x.appClientType == GENERAL] + client_list = [] + for c in client_devices: + client_id = utils.base64_url_encode(c.clientId) + short_client_id = shorten_client_id(app_info.clients, client_id, CLIENT_SHORT_ID_LENGTH) + client = ksm.ClientDevice( + name=c.id, + short_id=short_client_id, + created_on=int_to_datetime(c.createdOn), + expires_on=int_to_datetime(c.accessExpireOn), + first_access=int_to_datetime(c.firstAccess), + last_access=int_to_datetime(c.lastAccess), + ip_lock=c.lockIp, + ip_address=c.ipAddress + ) + client_list.append(client) + + shared_secrets = [] + for share in getattr(app_info, 'shares', []): + shared_secrets.append(handle_share_type(share, ksm_app, vault)) + + records_count = sum( + 1 for s in getattr(app_info, 'shares', []) + if ApplicationShareType.Name(s.shareType) == 'SHARE_TYPE_RECORD' + ) + + folders_count = sum( + 1 for s in getattr(app_info, 'shares', []) + if ApplicationShareType.Name(s.shareType) == 'SHARE_TYPE_FOLDER' + ) + + return ksm.SecretsManagerApp( + name=ksm_app.title, + uid=ksm_app.record_uid, + records=records_count, + folders=folders_count, + count=len(client_list), + last_access=None, + shared_secrets=shared_secrets, + client_devices=client_list + ) + + +def get_app_info(vault: vault_online.VaultOnline, app_uid): + rq = GetAppInfoRequest() + rq.appRecordUid.append(utils.base64_url_decode(app_uid)) + rs = vault.keeper_auth.execute_auth_rest( + request=rq, + rest_endpoint=URL_GET_APP_INFO_API, + response_type=GetAppInfoResponse + ) + return rs.appInfo + + +def shorten_client_id(all_clients, original_id, number_of_characters): + new_id = original_id[:number_of_characters] + res = [x for x in all_clients if utils.base64_url_encode(x.clientId).startswith(new_id)] + if len(res) == 1 or new_id == original_id: + return new_id + return shorten_client_id(all_clients, original_id, number_of_characters + 1) + + +def int_to_datetime(timestamp: int) -> datetime.datetime: + return datetime.datetime.fromtimestamp(timestamp / 1000) if timestamp and timestamp != 0 else None + +def handle_share_type(share, ksm_app, vault: vault_online.VaultOnline): + uid_str = utils.base64_url_encode(share.secretUid) + share_type = ApplicationShareType.Name(share.shareType) + editable_status = share.editable + + if share_type == 'SHARE_TYPE_RECORD': + return ksm.SharedSecretsInfo(type='RECORD', uid=uid_str, name=ksm_app.title, permissions=editable_status) + + elif share_type == 'SHARE_TYPE_FOLDER': + cached_sf = next((f for f in vault.vault_data.folders() if f.folder_uid == uid_str), None) + if cached_sf: + return ksm.SharedSecretsInfo(type='FOLDER', uid=uid_str, name=cached_sf.name, permissions=editable_status) + + else: + return ksm.SharedSecretsInfo(type='UNKOWN SHARE TYPE', uid=uid_str, name=ksm_app.title, permissions=editable_status) \ No newline at end of file diff --git a/keepersdk-package/unit_tests/test_ksm_management.py b/keepersdk-package/unit_tests/test_ksm_management.py new file mode 100644 index 00000000..dfcf4bd6 --- /dev/null +++ b/keepersdk-package/unit_tests/test_ksm_management.py @@ -0,0 +1,200 @@ +import datetime +import unittest +from unittest.mock import MagicMock, patch + +from keepersdk.vault import ksm_management + +class ListSecretsManagerAppsTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.vault.keeper_auth.execute_auth_rest.return_value.applicationSummary = [ + MagicMock( + appRecordUid=b'uid1', + folderRecords=2, + folderShares=1, + clientCount=3, + lastAccess=1710000000000 + ) + ] + self.vault.vault_data.load_record.return_value = MagicMock(title='App1') + self.patcher = patch('keepersdk.vault.ksm_management.utils.base64_url_encode', return_value='encoded_uid1') + self.mock_encode = self.patcher.start() + self.patcher_app = patch('keepersdk.vault.ksm_management.ksm.SecretsManagerApp', side_effect=lambda **kwargs: type('App', (), kwargs)) + self.mock_app = self.patcher_app.start() + + def tearDown(self): + self.patcher.stop() + self.patcher_app.stop() + + def test_returns_list_of_apps(self): + apps = ksm_management.list_secrets_manager_apps(self.vault) + self.assertEqual(len(apps), 1) + self.assertEqual(apps[0].name, 'App1') + self.assertEqual(apps[0].uid, 'encoded_uid1') + self.assertEqual(apps[0].records, 2) + self.assertEqual(apps[0].folders, 1) + self.assertEqual(apps[0].count, 3) + self.assertIsNotNone(apps[0].last_access) + + def test_empty_summary_returns_empty_list(self): + self.vault.keeper_auth.execute_auth_rest.return_value.applicationSummary = [] + apps = ksm_management.list_secrets_manager_apps(self.vault) + self.assertEqual(apps, []) + + def test_missing_app_record_sets_empty_name(self): + self.vault.vault_data.load_record.return_value = None + apps = ksm_management.list_secrets_manager_apps(self.vault) + self.assertEqual(apps[0].name, '') + + +class GetSecretsManagerAppTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.ksm_app = MagicMock(record_uid='uid1', title='App1') + self.vault.vault_data.records.return_value = [self.ksm_app] + self.patcher_encode = patch('keepersdk.vault.ksm_management.utils.base64_url_encode', return_value='encoded_uid1') + self.mock_encode = self.patcher_encode.start() + self.patcher_decode = patch('keepersdk.vault.ksm_management.utils.base64_url_decode', return_value=b'uid1') + self.mock_decode = self.patcher_decode.start() + self.patcher_client = patch('keepersdk.vault.ksm_management.ksm.ClientDevice', side_effect=lambda **kwargs: kwargs) + self.mock_client = self.patcher_client.start() + self.patcher_shared = patch('keepersdk.vault.ksm_management.ksm.SharedSecretsInfo', side_effect=lambda **kwargs: kwargs) + self.mock_shared = self.patcher_shared.start() + self.patcher_app = patch('keepersdk.vault.ksm_management.ksm.SecretsManagerApp', side_effect=lambda **kwargs: kwargs) + self.mock_app = self.patcher_app.start() + self.patcher_type = patch('keepersdk.vault.ksm_management.APIRequest_pb2.ApplicationShareType.Name', side_effect=lambda x: 'SHARE_TYPE_RECORD' if x == 1 else 'SHARE_TYPE_FOLDER' if x == 2 else 'UNKNOWN') + self.mock_type = self.patcher_type.start() + self.patcher_enterprise = patch('keepersdk.vault.ksm_management.GENERAL', 1) + self.mock_enterprise = self.patcher_enterprise.start() + self.patcher_short = patch('keepersdk.vault.ksm_management.shorten_client_id', return_value='shortid') + self.mock_short = self.patcher_short.start() + self.patcher_folders = patch('keepersdk.vault.ksm_management.vault_online.VaultOnline.vault_data', create=True) + self.mock_folders = self.patcher_folders.start() + + def tearDown(self): + self.patcher_encode.stop() + self.patcher_decode.stop() + self.patcher_client.stop() + self.patcher_shared.stop() + self.patcher_app.stop() + self.patcher_type.stop() + self.patcher_enterprise.stop() + self.patcher_short.stop() + self.patcher_folders.stop() + + def test_app_found_and_returns_app(self): + app_info = MagicMock() + client = MagicMock(appClientType=1, id='client1', createdOn=1710000000000, accessExpireOn=0, firstAccess=0, lastAccess=0, lockIp=False, ipAddress='1.2.3.4', clientId=b'clientid') + app_info.clients = [client] + share = MagicMock(secretUid=b'secret1', shareType=1, editable=True) + app_info.shares = [share] + with patch('keepersdk.vault.ksm_management.get_app_info', return_value=[app_info]): + result = ksm_management.get_secrets_manager_app(self.vault, 'uid1') + self.assertEqual(result['name'], 'App1') + self.assertIn('client_devices', result) + self.assertEqual(len(result['client_devices']), 1) + self.assertIsNone(result['last_access']) + self.assertEqual(result['records'], 1) + self.assertEqual(result['folders'], 0) + + def test_app_found_with_folder_share(self): + app_info = MagicMock() + client = MagicMock(appClientType=1, id='client1', createdOn=1710000000000, accessExpireOn=0, firstAccess=0, lastAccess=0, lockIp=False, ipAddress='1.2.3.4', clientId=b'clientid') + app_info.clients = [client] + share = MagicMock(secretUid=b'secret2', shareType=2, editable=False) + app_info.shares = [share] + with patch('keepersdk.vault.ksm_management.get_app_info', return_value=[app_info]): + result = ksm_management.get_secrets_manager_app(self.vault, 'uid1') + self.assertEqual(result['folders'], 1) + self.assertEqual(result['records'], 0) + + def test_app_not_found_raises(self): + self.vault.vault_data.records.return_value = [] + with self.assertRaises(ValueError): + ksm_management.get_secrets_manager_app(self.vault, 'notfound') + + def test_no_app_info_raises(self): + with patch('keepersdk.vault.ksm_management.get_app_info', return_value=[]): + with self.assertRaises(ValueError): + ksm_management.get_secrets_manager_app(self.vault, 'uid1') + + def test_handle_share_type_unknown(self): + app_info = MagicMock() + client = MagicMock(appClientType=1, id='client1', createdOn=1710000000000, accessExpireOn=0, firstAccess=0, lastAccess=0, lockIp=False, ipAddress='1.2.3.4', clientId=b'clientid') + app_info.clients = [client] + share = MagicMock(secretUid=b'secret3', shareType=99, editable=False) + app_info.shares = [share] + with patch('keepersdk.vault.ksm_management.get_app_info', return_value=[app_info]): + result = ksm_management.get_secrets_manager_app(self.vault, 'uid1') + self.assertEqual(result['records'], 0) + self.assertEqual(result['folders'], 0) + self.assertEqual(result['shared_secrets'][0]['type'], 'UNKOWN SHARE TYPE') + + +class GetAppInfoTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.patcher_decode = patch('keepersdk.vault.ksm_management.utils.base64_url_decode', return_value=b'uid1') + self.mock_decode = self.patcher_decode.start() + + def tearDown(self): + self.patcher_decode.stop() + + def test_calls_execute_auth_rest(self): + response = MagicMock() + response.appInfo = ['info'] + self.vault.keeper_auth.execute_auth_rest.return_value = response + result = ksm_management.get_app_info(self.vault, 'uid1') + self.assertEqual(result, ['info']) + + def test_empty_app_info(self): + response = MagicMock() + response.appInfo = [] + self.vault.keeper_auth.execute_auth_rest.return_value = response + result = ksm_management.get_app_info(self.vault, 'uid1') + self.assertEqual(result, []) + + def test_multiple_app_info(self): + response = MagicMock() + response.appInfo = ['info1', 'info2'] + self.vault.keeper_auth.execute_auth_rest.return_value = response + result = ksm_management.get_app_info(self.vault, 'uid1') + self.assertEqual(result, ['info1', 'info2']) + + +class ShortenClientIdTestCase(unittest.TestCase): + def setUp(self): + self.patcher_encode = patch('keepersdk.vault.ksm_management.utils.base64_url_encode', side_effect=lambda x: x.decode() if isinstance(x, bytes) else x) + self.mock_encode = self.patcher_encode.start() + + def tearDown(self): + self.patcher_encode.stop() + + def test_shorten_client_id_unique(self): + all_clients = [MagicMock(clientId=b'abc12345'), MagicMock(clientId=b'def67890')] + result = ksm_management.shorten_client_id(all_clients, 'abc12345', 3) + self.assertEqual(result, 'abc') + + def test_shorten_client_id_increase_length(self): + all_clients = [MagicMock(clientId=b'abc12345'), MagicMock(clientId=b'abc12346')] + # Should increase length until unique + result = ksm_management.shorten_client_id(all_clients, 'abc12345', 3) + self.assertTrue(result.startswith('abc12345')) + + +class IntToDatetimeTestCase(unittest.TestCase): + def test_valid_timestamp(self): + dt = ksm_management.int_to_datetime(1710000000000) + self.assertIsInstance(dt, datetime.datetime) + + def test_zero_timestamp(self): + dt = ksm_management.int_to_datetime(0) + self.assertIsNone(dt) + + def test_none_timestamp(self): + dt = ksm_management.int_to_datetime(None) + self.assertIsNone(dt) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 231a707449e2f0511ed2ea04ea88ce6c892a938e Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 19 Jun 2025 10:36:52 +0530 Subject: [PATCH 08/44] Corrected imports --- .../src/keepersdk/vault/ksm_management.py | 18 ++++++------------ .../unit_tests/test_ksm_management.py | 14 +------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index dd46a95d..ed3499a3 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -1,8 +1,6 @@ import datetime -import time from . import vault_online, ksm -from ..proto import APIRequest_pb2 from ..proto.APIRequest_pb2 import GetApplicationsSummaryResponse, ApplicationShareType, GetAppInfoRequest, GetAppInfoResponse from ..proto.enterprise_pb2 import GENERAL from .. import utils @@ -23,7 +21,7 @@ def list_secrets_manager_apps(vault: vault_online.VaultOnline) -> list[ksm.Secre for app_summary in response.applicationSummary: uid = utils.base64_url_encode(app_summary.appRecordUid) app_record = vault.vault_data.load_record(uid) - name = getattr(app_record, 'title', '') if app_record else '' + name = app_record.title if app_record else '' last_access = int_to_datetime(app_summary.lastAccess) secrets_app = ksm.SecretsManagerApp( name=name, @@ -69,15 +67,11 @@ def get_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str) - for share in getattr(app_info, 'shares', []): shared_secrets.append(handle_share_type(share, ksm_app, vault)) - records_count = sum( - 1 for s in getattr(app_info, 'shares', []) + records_count = len([ + s for s in getattr(app_info, 'shares', []) if ApplicationShareType.Name(s.shareType) == 'SHARE_TYPE_RECORD' - ) - - folders_count = sum( - 1 for s in getattr(app_info, 'shares', []) - if ApplicationShareType.Name(s.shareType) == 'SHARE_TYPE_FOLDER' - ) + ]) + folders_count = len(shared_secrets) - records_count return ksm.SecretsManagerApp( name=ksm_app.title, @@ -127,4 +121,4 @@ def handle_share_type(share, ksm_app, vault: vault_online.VaultOnline): return ksm.SharedSecretsInfo(type='FOLDER', uid=uid_str, name=cached_sf.name, permissions=editable_status) else: - return ksm.SharedSecretsInfo(type='UNKOWN SHARE TYPE', uid=uid_str, name=ksm_app.title, permissions=editable_status) \ No newline at end of file + return None \ No newline at end of file diff --git a/keepersdk-package/unit_tests/test_ksm_management.py b/keepersdk-package/unit_tests/test_ksm_management.py index dfcf4bd6..5efa7c21 100644 --- a/keepersdk-package/unit_tests/test_ksm_management.py +++ b/keepersdk-package/unit_tests/test_ksm_management.py @@ -62,7 +62,7 @@ def setUp(self): self.mock_shared = self.patcher_shared.start() self.patcher_app = patch('keepersdk.vault.ksm_management.ksm.SecretsManagerApp', side_effect=lambda **kwargs: kwargs) self.mock_app = self.patcher_app.start() - self.patcher_type = patch('keepersdk.vault.ksm_management.APIRequest_pb2.ApplicationShareType.Name', side_effect=lambda x: 'SHARE_TYPE_RECORD' if x == 1 else 'SHARE_TYPE_FOLDER' if x == 2 else 'UNKNOWN') + self.patcher_type = patch('keepersdk.proto.APIRequest_pb2.ApplicationShareType.Name', side_effect=lambda x: 'SHARE_TYPE_RECORD' if x == 1 else 'SHARE_TYPE_FOLDER' if x == 2 else 'UNKNOWN') self.mock_type = self.patcher_type.start() self.patcher_enterprise = patch('keepersdk.vault.ksm_management.GENERAL', 1) self.mock_enterprise = self.patcher_enterprise.start() @@ -118,18 +118,6 @@ def test_no_app_info_raises(self): with self.assertRaises(ValueError): ksm_management.get_secrets_manager_app(self.vault, 'uid1') - def test_handle_share_type_unknown(self): - app_info = MagicMock() - client = MagicMock(appClientType=1, id='client1', createdOn=1710000000000, accessExpireOn=0, firstAccess=0, lastAccess=0, lockIp=False, ipAddress='1.2.3.4', clientId=b'clientid') - app_info.clients = [client] - share = MagicMock(secretUid=b'secret3', shareType=99, editable=False) - app_info.shares = [share] - with patch('keepersdk.vault.ksm_management.get_app_info', return_value=[app_info]): - result = ksm_management.get_secrets_manager_app(self.vault, 'uid1') - self.assertEqual(result['records'], 0) - self.assertEqual(result['folders'], 0) - self.assertEqual(result['shared_secrets'][0]['type'], 'UNKOWN SHARE TYPE') - class GetAppInfoTestCase(unittest.TestCase): def setUp(self): From 4db4ccb7b36a0c98eafedd1e7b65969627c595f8 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 23 Jun 2025 15:52:36 +0530 Subject: [PATCH 09/44] secrets-manager-app create and remove commands --- .../src/keepercli/commands/secrets_manager.py | 67 ++++++++++---- .../src/keepersdk/vault/ksm_management.py | 57 +++++++++++- .../unit_tests/test_ksm_management.py | 88 +++++++++++++++++++ 3 files changed, 195 insertions(+), 17 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index 21ff64a4..6ea46c67 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -1,4 +1,5 @@ import argparse +from typing import Optional from keepersdk.vault import ksm_management @@ -30,9 +31,6 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser): parser.add_argument( '--name', '-n', type=str, dest='name', action='store', required=False, help='Application Name or UID' ) - parser.add_argument( - '--purge', '-p', action='store_true', help='remove the record from all folders and purge it from the trash' - ) parser.add_argument( '-f', '--force', dest='force', action='store_true', help='Force add or remove app' ) @@ -48,20 +46,39 @@ def execute(self, context: KeeperParams, **kwargs) -> None: raise ValueError("Vault is not initialized.") command = kwargs.get('command') + uid_or_name = kwargs.get('name') + force = kwargs.get('force') - if command == 'list': - return self.list_app(context.vault) + def list_app(): + return self.list_app(vault=context.vault) - elif command == 'get': - app_name = kwargs.get('name') - return self.get_app(context.vault, app_name) - + def get_app(): + return self.get_app(vault=context.vault, uid_or_name=uid_or_name) + + def create_app(): + self.create_app(vault=context.vault, name=uid_or_name, force=force) + return context.vault_down() + + def remove_app(): + self.remove_app(vault=context.vault, uid_or_name=uid_or_name, force=force) + return + + command_map = { + 'list': list_app, + 'get': get_app, + 'create': create_app, + 'remove': remove_app, + } + + action = command_map.get(command) + if action: + return action() else: - logger.error(f"Unknown command '{command}'. Available commands: list, get, create, remove, share and unshare.") + logger.error(f"Unknown command '{command}'. Available commands: list, get, create, remove") return - def list_app(self, vault): + def list_app(self, vault: KeeperParams.vault): app_list = ksm_management.list_secrets_manager_apps(vault) headers = ['App name', 'App UID', 'Records', 'Folders', 'Devices', 'Last Access'] rows = [ @@ -71,12 +88,12 @@ def list_app(self, vault): report_utils.dump_report_data(rows, headers=headers, fmt='table') - def get_app(self, vault, app_name): - if not app_name: + def get_app(self, vault: KeeperParams.vault, uid_or_name: str): + if not uid_or_name: logger.error("Application name or UID is required for 'app get'. Use --name='example' to set it.") return - app = ksm_management.get_secrets_manager_app(vault=vault, uid_or_name=app_name) + app = ksm_management.get_secrets_manager_app(vault=vault, uid_or_name=uid_or_name) logger.info(f'\nSecrets Manager Application\n' f'App Name: {app.name}\n' @@ -91,4 +108,24 @@ def get_app(self, vault, app_name): ksm_utils.print_shared_secrets_info(app.shared_secrets) else: logger.info('\tThere are no shared secrets to this application') - return \ No newline at end of file + return + + + def create_app(self, vault: KeeperParams.vault, name: str, force: Optional[bool] = False): + if not name: + logger.error("Application name or UID is required for 'app create'. Use --name='example' to set it.") + return + + app_uid = ksm_management.create_secrets_manager_app(vault=vault, name=name, force_add=force) + + logger.info(f'Application was successfully added (UID: {app_uid})') + + + def remove_app(self, vault: KeeperParams.vault, uid_or_name: str, force: Optional[bool] = False): + if not uid_or_name: + logger.error("Application name or UID is required for 'app remove'. Use --name='example' to set it.") + return + + app_uid = ksm_management.remove_secrets_manager_app(vault=vault, uid_or_name=uid_or_name, force=force) + + logger.info(f'Application was successfully removed (UID: {app_uid})') \ No newline at end of file diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index ed3499a3..b72628a1 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -1,12 +1,16 @@ import datetime +import json +from typing import Optional -from . import vault_online, ksm +from . import vault_online, ksm, record_management, vault_types from ..proto.APIRequest_pb2 import GetApplicationsSummaryResponse, ApplicationShareType, GetAppInfoRequest, GetAppInfoResponse from ..proto.enterprise_pb2 import GENERAL -from .. import utils +from ..proto.record_pb2 import ApplicationAddRequest +from .. import utils, crypto URL_GET_SUMMARY_API = 'vault/get_applications_summary' URL_GET_APP_INFO_API = 'vault/get_app_info' +URL_CREATE_APP_API = 'vault/application_add' CLIENT_SHORT_ID_LENGTH = 8 @@ -85,6 +89,55 @@ def get_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str) - ) +def create_secrets_manager_app(vault: vault_online.VaultOnline, name: str, force_add: Optional[bool] = False): + + existing_app = next((r for r in vault.vault_data.records() if r.title == name), None) + if existing_app and not force_add: + raise ValueError(f'Application with the same name {name} already exists.') + + app_record_data = { + 'title': name, + 'type': 'app' + } + + data_json = json.dumps(app_record_data) + record_key_unencrypted = utils.generate_aes_key() + record_key_encrypted = crypto.encrypt_aes_v2(record_key_unencrypted, vault.keeper_auth.auth_context.data_key) + + app_record_uid_str = utils.generate_uid() + app_record_uid = utils.base64_url_decode(app_record_uid_str) + + rdata = bytes(data_json, 'utf-8') + rdata = crypto.encrypt_aes_v2(rdata, record_key_unencrypted) + + client_modified_time = utils.current_milli_time() + + ra = ApplicationAddRequest() + ra.app_uid = app_record_uid + ra.record_key = record_key_encrypted + ra.client_modified_time = client_modified_time + ra.data = rdata + + vault.keeper_auth.execute_auth_rest(request=ra, rest_endpoint=URL_CREATE_APP_API, response_type=None) + + app_uid_str = utils.base64_url_encode(ra.app_uid) + return app_uid_str + + +def remove_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str, force: Optional[bool] = False): + + app = get_secrets_manager_app(vault=vault, uid_or_name=uid_or_name) + + if (app.records != 0 or app.folders != 0 or app.count != 0) and not force: + raise ValueError('Cannot remove application with clients, shared record, shared folder. Force remove to proceed') + + record_obj = vault_types.RecordPath(folder_uid=None, record_uid=app.uid) + + record_management.delete_vault_objects(vault=vault, vault_objects=[record_obj]) + + return app.uid + + def get_app_info(vault: vault_online.VaultOnline, app_uid): rq = GetAppInfoRequest() rq.appRecordUid.append(utils.base64_url_decode(app_uid)) diff --git a/keepersdk-package/unit_tests/test_ksm_management.py b/keepersdk-package/unit_tests/test_ksm_management.py index 5efa7c21..69789595 100644 --- a/keepersdk-package/unit_tests/test_ksm_management.py +++ b/keepersdk-package/unit_tests/test_ksm_management.py @@ -184,5 +184,93 @@ def test_none_timestamp(self): self.assertIsNone(dt) +class CreateSecretsManagerAppTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.vault.vault_data.records.return_value = [] + self.vault.keeper_auth.auth_context.data_key = b'datakey' + self.patcher_uid = patch('keepersdk.vault.ksm_management.utils.generate_uid', return_value='uidstr') + self.mock_uid = self.patcher_uid.start() + self.patcher_decode = patch('keepersdk.vault.ksm_management.utils.base64_url_decode', return_value=b'uidbytes') + self.mock_decode = self.patcher_decode.start() + self.patcher_encode = patch('keepersdk.vault.ksm_management.utils.base64_url_encode', return_value='encoded_uid') + self.mock_encode = self.patcher_encode.start() + self.patcher_aes = patch('keepersdk.vault.ksm_management.utils.generate_aes_key', return_value=b'aeskey') + self.mock_aes = self.patcher_aes.start() + self.patcher_encrypt = patch('keepersdk.vault.ksm_management.crypto.encrypt_aes_v2', side_effect=lambda data, key: b'encrypted_' + data if isinstance(data, bytes) else b'encrypted_' + data.encode()) + self.mock_encrypt = self.patcher_encrypt.start() + self.patcher_time = patch('keepersdk.vault.ksm_management.utils.current_milli_time', return_value=1710000000000) + self.mock_time = self.patcher_time.start() + self.patcher_req = patch('keepersdk.vault.ksm_management.ApplicationAddRequest', autospec=True) + self.mock_req = self.patcher_req.start() + + def tearDown(self): + self.patcher_uid.stop() + self.patcher_decode.stop() + self.patcher_encode.stop() + self.patcher_aes.stop() + self.patcher_encrypt.stop() + self.patcher_time.stop() + self.patcher_req.stop() + + def test_create_app_success(self): + app_uid = ksm_management.create_secrets_manager_app(self.vault, 'TestApp') + self.assertEqual(app_uid, 'encoded_uid') + self.vault.keeper_auth.execute_auth_rest.assert_called_once() + self.mock_req.assert_called_once() + args, kwargs = self.vault.keeper_auth.execute_auth_rest.call_args + self.assertEqual(kwargs.get('rest_endpoint'), ksm_management.URL_CREATE_APP_API) + + def test_create_app_duplicate_raises(self): + mock_record = MagicMock(title='TestApp') + self.vault.vault_data.records.return_value = [mock_record] + with self.assertRaises(ValueError) as cm: + ksm_management.create_secrets_manager_app(self.vault, 'TestApp') + self.assertEqual(str(cm.exception), 'Application with the same name TestApp already exists.') + + def test_create_app_duplicate_force_add(self): + mock_record = MagicMock(title='TestApp') + self.vault.vault_data.records.return_value = [mock_record] + app_uid = ksm_management.create_secrets_manager_app(self.vault, 'TestApp', force_add=True) + self.assertEqual(app_uid, 'encoded_uid') + + +class RemoveSecretsManagerAppTestCase(unittest.TestCase): + def setUp(self): + self.vault = MagicMock() + self.patcher_get = patch('keepersdk.vault.ksm_management.get_secrets_manager_app') + self.mock_get = self.patcher_get.start() + self.patcher_delete = patch('keepersdk.vault.ksm_management.record_management.delete_vault_objects') + self.mock_delete = self.patcher_delete.start() + self.patcher_path = patch('keepersdk.vault.ksm_management.vault_types.RecordPath', side_effect=lambda folder_uid, record_uid: MagicMock(folder_uid=folder_uid, record_uid=record_uid)) + self.mock_path = self.patcher_path.start() + + def tearDown(self): + self.patcher_get.stop() + self.patcher_delete.stop() + self.patcher_path.stop() + + def test_remove_app_success(self): + app = MagicMock(uid='appuid', records=0, folders=0, count=0) + self.mock_get.return_value = app + uid = ksm_management.remove_secrets_manager_app(self.vault, 'appuid') + self.mock_delete.assert_called_once() + self.assertEqual(uid, 'appuid') + + def test_remove_app_with_clients_raises(self): + app = MagicMock(uid='appuid', records=1, folders=0, count=0) + self.mock_get.return_value = app + with self.assertRaises(ValueError) as cm: + ksm_management.remove_secrets_manager_app(self.vault, 'appuid') + self.assertEqual(str(cm.exception), 'Cannot remove application with clients, shared record, shared folder. Force remove to proceed') + + def test_remove_app_with_clients_force(self): + app = MagicMock(uid='appuid', records=1, folders=1, count=1) + self.mock_get.return_value = app + uid = ksm_management.remove_secrets_manager_app(self.vault, 'appuid', force=True) + self.mock_delete.assert_called_once() + self.assertEqual(uid, 'appuid') + + if __name__ == "__main__": unittest.main() \ No newline at end of file From 67c0a0d6d177e38cdc4948613802fcbc4cf781d2 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 23 Jun 2025 18:15:29 +0530 Subject: [PATCH 10/44] download-record-types bug fix --- keepersdk-package/src/keepersdk/importer/keeper_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keepersdk-package/src/keepersdk/importer/keeper_format.py b/keepersdk-package/src/keepersdk/importer/keeper_format.py index 0411a1ec..46c5edfd 100644 --- a/keepersdk-package/src/keepersdk/importer/keeper_format.py +++ b/keepersdk-package/src/keepersdk/importer/keeper_format.py @@ -509,4 +509,4 @@ def download_record_type(self, **kwargs) -> Iterable[import_data.RecordType]: if isinstance(ertf.required, bool): rtf.required = ertf.required rt.fields.append(rtf) - yield rt + yield rt From 199253002540a7a2c23e025a7e1ba01da65ce0c4 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 25 Jun 2025 18:02:28 +0530 Subject: [PATCH 11/44] Yubikey login method bug fix --- keepercli-package/src/keepercli/login.py | 2 +- .../src/keepersdk/authentication/yubikey.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/keepercli-package/src/keepercli/login.py b/keepercli-package/src/keepercli/login.py index ee6babfa..29e55efb 100644 --- a/keepercli-package/src/keepercli/login.py +++ b/keepercli-package/src/keepercli/login.py @@ -313,7 +313,7 @@ def handle_two_factor(context: KeeperParams, step: login_auth.LoginStepTwoFactor challenge = json.loads(channel.challenge) signature = yubikey_authenticate(challenge, FidoCliInteraction()) if signature: - prompt_utils.output_text('') + prompt_utils.output_text('Verified Security Key.') step.send_code(channel.channel_uid, signature) break except Exception as e: diff --git a/keepersdk-package/src/keepersdk/authentication/yubikey.py b/keepersdk-package/src/keepersdk/authentication/yubikey.py index 5c6a4600..bedf9280 100644 --- a/keepersdk-package/src/keepersdk/authentication/yubikey.py +++ b/keepersdk-package/src/keepersdk/authentication/yubikey.py @@ -27,6 +27,11 @@ def yubikey_authenticate(request: Dict[str, Any], user_interaction: UserInteract origin = '' options = request['publicKeyCredentialRequestOptions'] + + if 'extensions' not in options: + options['extensions'] = {} + if 'largeBlob' not in options['extensions']: + options['extensions']['largeBlob'] = {'read': False} if 'extensions' in options: extensions = options['extensions'] origin = extensions.get('appid') or '' @@ -94,15 +99,14 @@ def yubikey_authenticate(request: Dict[str, Any], user_interaction: UserInteract evt.set() if response: - credential_id = utils.base64_url_encode(response.credential_id or b'') - extensions = dict(response.extension_results) if response.extension_results else {} + extensions = dict(response.client_extension_results) if response.client_extension_results else {} signature = { - "id": credential_id, - "rawId": credential_id, + "id": response.id, + "rawId": utils.base64_url_encode(response.raw_id), "response": { - "authenticatorData": utils.base64_url_encode(response.authenticator_data), - "clientDataJSON": response.client_data.b64, - "signature": utils.base64_url_encode(response.signature), + "authenticatorData": utils.base64_url_encode(response.response.authenticator_data), + "clientDataJSON": response.response.client_data.b64, + "signature": utils.base64_url_encode(response.response.signature), }, "type": "public-key", "clientExtensionResults": extensions From 672a8f1abde98c4da97877a0d247a20983be1f89 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 27 Jun 2025 15:39:02 +0530 Subject: [PATCH 12/44] Bug fix in delete-attachment command and added rm command --- .../src/keepercli/commands/record_edit.py | 41 +++++++++++++++++++ .../src/keepercli/helpers/record_utils.py | 4 ++ .../src/keepercli/register_commands.py | 3 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index 2768542e..e23d53e3 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -985,3 +985,44 @@ def execute(self, context, **kwargs): attachment.upload_attachments(context.vault, record, upload_tasks) record_management.update_record(context.vault, record) + + +class RecordDeleteCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='rm', + description='Remove a record' + ) + RecordDeleteCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '-f', '--force', dest='force', action='store_true', help='do not prompt' + ) + parser.add_argument( + 'records', nargs='*', type=str, help='record path or UID. Can be multiple.' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + record_uids = kwargs.get('records') + force = kwargs.get('force') or False + + if not isinstance(record_uids, list): + if isinstance(record_uids, str): + record_uids = [record_uids] + else: + record_uids = [] + + confirm_fn = None if force else record_utils.default_confirm + + record_management.delete_vault_objects( + vault=context.vault, + vault_objects=record_uids, + confirm=confirm_fn + ) + diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index 61925c1a..28c01e04 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -53,3 +53,7 @@ def add_folder(f: vault_types.Folder) -> None: add_folder(folder) for folder in folders: yield from folder.records + + +def default_confirm(prompt: str) -> bool: + return input(f"{prompt} (y/n): ").strip().lower() == 'y' \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 1d227999..0b959732 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -38,7 +38,8 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('shortcut', vault_record.ShortcutCommand(), base.CommandScope.Vault) commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') commands.register_command('record-update', record_edit.RecordUpdateCommand(), base.CommandScope.Vault, 'ru') - commands.register_command('delete-attachment', record_edit.RecordUpdateCommand(), base.CommandScope.Vault) + commands.register_command('rm', record_edit.RecordDeleteCommand(), base.CommandScope.Vault) + commands.register_command('delete-attachment', record_edit.RecordDeleteAttachmentCommand(), base.CommandScope.Vault) commands.register_command('download-attachment', record_edit.RecordDownloadAttachmentCommand(), base.CommandScope.Vault, 'da') commands.register_command('upload-attachment', record_edit.RecordUploadAttachmentCommand(), base.CommandScope.Vault, 'ua') commands.register_command('import', importer_commands.ImportCommand(), base.CommandScope.Vault) From 33a1d5f2138af91c5cdaf6e24daf1e61379d458d Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 16 Jul 2025 12:38:30 +0530 Subject: [PATCH 13/44] Secrets Manager App Share-Unshare, Share Record and Share Folder commands and YubiKey fix --- .../src/keepercli/commands/secrets_manager.py | 306 ++++- .../keepercli/commands/share_management.py | 1019 +++++++++++++++++ .../src/keepercli/helpers/share_utils.py | 439 +++++++ .../src/keepercli/register_commands.py | 4 +- .../src/keepersdk/authentication/yubikey.py | 6 +- .../src/keepersdk/vault/ksm_management.py | 2 +- 6 files changed, 1738 insertions(+), 38 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/share_management.py create mode 100644 keepercli-package/src/keepercli/helpers/share_utils.py diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index 6ea46c67..e8f88c17 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -1,17 +1,28 @@ import argparse +from enum import Enum from typing import Optional -from keepersdk.vault import ksm_management +from keepersdk.vault import ksm_management, vault_online from . import base +from .share_management import ShareAction, ShareRecordCommand, ShareFolderCommand from .. import api +from ..helpers import ksm_utils, report_utils, share_utils from ..params import KeeperParams -from ..helpers import report_utils, ksm_utils logger = api.get_logger() +class SecretsManagerCommand(Enum): + LIST = "list" + GET = "get" + CREATE = "create" + REMOVE = "remove" + SHARE = "share" + UNSHARE = "unshare" + + class SecretsManagerAppCommand(base.ArgparseCommand): def __init__(self): @@ -22,10 +33,12 @@ def __init__(self): SecretsManagerAppCommand.add_arguments_to_parser(self.parser) super().__init__(self.parser) + @staticmethod def add_arguments_to_parser(parser: argparse.ArgumentParser): parser.add_argument( '--command', type=str, action='store', required=True, dest='command', + choices=[cmd.value for cmd in SecretsManagerCommand], help='One of: "list", "get", "create", "remove", "share" or "unshare"' ) parser.add_argument( @@ -45,40 +58,58 @@ def execute(self, context: KeeperParams, **kwargs) -> None: if not context.vault: raise ValueError("Vault is not initialized.") + vault = context.vault command = kwargs.get('command') uid_or_name = kwargs.get('name') force = kwargs.get('force') + email = kwargs.get('email') + is_admin = kwargs.get('admin', False) + + if not command: + raise ValueError("Command is required. Available commands: list, get, create, remove, share, unshare") + + if command != SecretsManagerCommand.LIST.value and not uid_or_name: + raise ValueError("Application name or UID is required. Use --name='example' to set it.") def list_app(): - return self.list_app(vault=context.vault) + return self.list_app(vault=vault) def get_app(): - return self.get_app(vault=context.vault, uid_or_name=uid_or_name) + return self.get_app(vault=vault, uid_or_name=uid_or_name) def create_app(): - self.create_app(vault=context.vault, name=uid_or_name, force=force) + self.create_app(vault=vault, name=uid_or_name, force=force) return context.vault_down() def remove_app(): - self.remove_app(vault=context.vault, uid_or_name=uid_or_name, force=force) + self.remove_app(vault=vault, uid_or_name=uid_or_name, force=force) return + + def share_app(): + self.share_app(context=context, uid_or_name=uid_or_name, unshare=False, email=email, is_admin=is_admin) + return context.vault_down() + + def unshare_app(): + self.share_app(context=context, uid_or_name=uid_or_name, unshare=True, email=email, is_admin=is_admin) + return context.vault_down() command_map = { - 'list': list_app, - 'get': get_app, - 'create': create_app, - 'remove': remove_app, + SecretsManagerCommand.LIST.value: list_app, + SecretsManagerCommand.GET.value: get_app, + SecretsManagerCommand.CREATE.value: create_app, + SecretsManagerCommand.REMOVE.value: remove_app, + SecretsManagerCommand.SHARE.value: share_app, + SecretsManagerCommand.UNSHARE.value: unshare_app } - + action = command_map.get(command) if action: return action() else: - logger.error(f"Unknown command '{command}'. Available commands: list, get, create, remove") - return + raise ValueError(f"Unknown command '{command}'. Available commands: {', '.join([cmd.value for cmd in SecretsManagerCommand])}") - def list_app(self, vault: KeeperParams.vault): + def list_app(self, vault: vault_online.VaultOnline): app_list = ksm_management.list_secrets_manager_apps(vault) headers = ['App name', 'App UID', 'Records', 'Folders', 'Devices', 'Last Access'] rows = [ @@ -88,18 +119,13 @@ def list_app(self, vault: KeeperParams.vault): report_utils.dump_report_data(rows, headers=headers, fmt='table') - def get_app(self, vault: KeeperParams.vault, uid_or_name: str): - if not uid_or_name: - logger.error("Application name or UID is required for 'app get'. Use --name='example' to set it.") - return - + def get_app(self, vault: vault_online.VaultOnline, uid_or_name: str): app = ksm_management.get_secrets_manager_app(vault=vault, uid_or_name=uid_or_name) - logger.info(f'\nSecrets Manager Application\n' f'App Name: {app.name}\n' f'App UID: {app.uid}') - if len(app.client_devices) > 0: + if app.client_devices and len(app.client_devices) > 0: ksm_utils.print_client_device_info(app.client_devices) else: logger.info('\nNo client devices registered for this Application\n') @@ -111,21 +137,237 @@ def get_app(self, vault: KeeperParams.vault, uid_or_name: str): return - def create_app(self, vault: KeeperParams.vault, name: str, force: Optional[bool] = False): - if not name: - logger.error("Application name or UID is required for 'app create'. Use --name='example' to set it.") - return - + def create_app(self, vault: vault_online.VaultOnline, name: str, force: Optional[bool] = False): app_uid = ksm_management.create_secrets_manager_app(vault=vault, name=name, force_add=force) - logger.info(f'Application was successfully added (UID: {app_uid})') - def remove_app(self, vault: KeeperParams.vault, uid_or_name: str, force: Optional[bool] = False): - if not uid_or_name: - logger.error("Application name or UID is required for 'app remove'. Use --name='example' to set it.") + def remove_app(self, vault: vault_online.VaultOnline, uid_or_name: str, force: Optional[bool] = False): + app_uid = ksm_management.remove_secrets_manager_app(vault=vault, uid_or_name=uid_or_name, force=force) + logger.info(f'Application was successfully removed (UID: {app_uid})') + + def share_app(self, context: KeeperParams, uid_or_name: str, unshare: bool = False, + email: Optional[str] = None, is_admin: Optional[bool] = False): + if not email: + raise ValueError("Email parameter is required for sharing. Use --email='user@example.com' to set it.") + + app_record = next((r for r in context.vault.vault_data.records() if r.record_uid == uid_or_name or r.title == uid_or_name), None) + + if not app_record: + raise ValueError(f'No application found with UID/Name: {uid_or_name}') + + app_uid = app_record.record_uid + action = ShareAction.REVOKE.value if unshare else ShareAction.GRANT.value + emails = [email] + can_edit=is_admin and not unshare + can_share=is_admin and not unshare + args = { + "action": action, + "email": emails, + "record": app_uid, + "can_edit": can_edit, + "can_share": can_share + } + + share_record_command = ShareRecordCommand() + share_record_command.execute(context=context, **args) + + context.vault.sync_down() + + SecretsManagerAppCommand.update_shares_user_permissions(context=context, uid=app_uid, removed=unshare) + + @staticmethod + def update_shares_user_permissions(context: KeeperParams, uid: str, removed: bool): + + vault = context.vault + + # Get user permissions for the app + user_perms = SecretsManagerAppCommand._get_app_user_permissions(vault, uid) + + # Get app info and shared secrets + app_infos = ksm_management.get_app_info(vault=vault, app_uid=uid) + app_info = app_infos[0] + if not app_info: return + + # Separate shared records and folders + shared_recs, shared_folders = SecretsManagerAppCommand._separate_shared_items( + vault, app_info.shares + ) - app_uid = ksm_management.remove_secrets_manager_app(vault=vault, uid_or_name=uid_or_name, force=force) + # Create share requests for users that need updates + SecretsManagerAppCommand._process_share_updates( + context, vault, user_perms, shared_recs, shared_folders, removed + ) + + @staticmethod + def _get_app_user_permissions(vault: vault_online.VaultOnline, uid: str) -> list: + """Get user permissions for the application.""" + share_info = share_utils.get_record_shares(vault=vault, record_uids=[uid], is_share_admin=False) + user_perms = [] + if share_info: + for record_info in share_info: + if record_info.get('record_uid') == uid: + user_perms = record_info.get('shares', {}).get('user_permissions', []) + break + return user_perms + + @staticmethod + def _separate_shared_items(vault: vault_online.VaultOnline, shared_secrets): + """Separate shared secrets into records and folders.""" + from keepersdk.proto.APIRequest_pb2 import ApplicationShareType + from keepersdk import utils + shared_recs = [] + shared_folders = [] + for share in shared_secrets: + uid_str = utils.base64_url_encode(share.secretUid) + share_type = ApplicationShareType.Name(share.shareType) + if share_type == 'SHARE_TYPE_RECORD': + shared_recs.append(uid_str) + elif share_type == 'SHARE_TYPE_FOLDER': + shared_folders.append(uid_str) + + if shared_recs: + share_utils.get_record_shares(vault=vault, record_uids=shared_recs, is_share_admin=False) + + return shared_recs, shared_folders + + @staticmethod + def _process_share_updates(context: KeeperParams, vault: vault_online.VaultOnline, + user_perms: list, shared_recs: list, shared_folders: list, removed: bool): + """Process share updates for users.""" + # Get admin and viewer users + admins = [up.get('username') for up in user_perms if up.get('editable')] + admins = [x for x in admins if x != vault.keeper_auth.auth_context.username] + viewers = [up.get('username') for up in user_perms if not up.get('editable')] + app_users_map = dict(admins=admins, viewers=viewers) + + # Create share requests + sf_requests = [] + rec_requests = [] + + for group, users in app_users_map.items(): + users_needing_update = [ + u for u in users + if SecretsManagerAppCommand._user_needs_update(vault, u, shared_recs + shared_folders, removed) + ] + + if not users_needing_update: + continue + + # Process folder share requests + folder_requests = SecretsManagerAppCommand._create_folder_share_requests( + vault, shared_folders, users_needing_update, removed + ) + sf_requests.append(folder_requests) + + # Process record share requests + record_requests = SecretsManagerAppCommand._create_record_share_requests( + context, shared_recs, users_needing_update, removed + ) + rec_requests.extend(record_requests) + + if sf_requests: + ShareFolderCommand.send_requests(vault, sf_requests) + if rec_requests: + ShareRecordCommand.send_requests(vault, rec_requests) + logger.info("Share updates processed successfully") + + @staticmethod + def _user_needs_update(vault: vault_online.VaultOnline, user: str, share_uids: list, removed: bool) -> bool: + """Check if a user needs share permission updates.""" + # Get the share information for records + record_share_info = share_utils.get_record_shares(vault=vault, record_uids=share_uids, is_share_admin=False) + record_permissions = {} + if record_share_info: + for record_info in record_share_info: + record_uid = record_info.get('record_uid') + if record_uid: + record_permissions[record_uid] = record_info.get('shares', {}).get('user_permissions', []) + + record_cache = {x.record_uid: x for x in vault.vault_data.records()} + + for share_uid in share_uids: + is_rec_share = share_uid in record_cache + + if is_rec_share: + # Use the permissions we fetched above + share_user_permissions = record_permissions.get(share_uid, []) + else: + # For shared folders, get users from the folder object + shared_folder_obj = vault.vault_data.load_shared_folder(shared_folder_uid=share_uid) + if shared_folder_obj and shared_folder_obj.user_permissions: + share_user_permissions = shared_folder_obj.user_permissions + else: + share_user_permissions = [] + + # Check if user already has permissions + if not any(up.get('username') == user for up in share_user_permissions if isinstance(up, dict)): + return True + return False + + @staticmethod + def _create_folder_share_requests(vault: vault_online.VaultOnline, shared_folders: list, + users: list, removed: bool) -> list: + """Create folder share requests.""" + if not shared_folders: + return [] + + sf_action = ShareAction.REMOVE.value if removed else ShareAction.GRANT.value + + requests = [] + for folder_uid in shared_folders: + for user in users: + if SecretsManagerAppCommand._user_needs_update(vault, user, [folder_uid], removed): + sh_fol = vault.vault_data.load_shared_folder(folder_uid) + shared_folder_revision = vault.vault_data.storage.shared_folders.get_entity(folder_uid).revision + sf_unencrypted_key = vault.vault_data.get_shared_folder_key(shared_folder_uid=folder_uid) + sf_info = { + 'shared_folder_uid': folder_uid, + 'users': sh_fol.user_permissions, + 'teams': [], + 'records': sh_fol.record_permissions, + 'shared_folder_key_unencrypted': sf_unencrypted_key, + 'default_manage_users': sh_fol.default_can_share, + 'default_manage_records': sh_fol.default_can_edit, + 'revision': shared_folder_revision + } + request = ShareFolderCommand.prepare_request( + vault=vault, + kwargs={'action': sf_action}, + curr_sf=sf_info, + users=[user], + teams=[], + rec_uids=[], + default_record=False, + default_account=False, + share_expiration=-1 + ) + requests.append(request) + return requests + + @staticmethod + def _create_record_share_requests(context: KeeperParams, shared_recs: list, + users: list, removed: bool) -> list: + """Create record share requests.""" + if not shared_recs or not context.vault: + return [] + + rec_action = ShareAction.REVOKE.value if removed else ShareAction.GRANT.value - logger.info(f'Application was successfully removed (UID: {app_uid})') \ No newline at end of file + requests = [] + for record_uid in shared_recs: + for user in users: + if SecretsManagerAppCommand._user_needs_update(context.vault, user, [record_uid], removed): + request = ShareRecordCommand.prep_request( + context=context, + emails=[user], + action=rec_action, + uid_or_name=record_uid, + share_expiration=-1, + dry_run=False, + can_edit=False, + can_share=False + ) + requests.append(request) + return requests diff --git a/keepercli-package/src/keepercli/commands/share_management.py b/keepercli-package/src/keepercli/commands/share_management.py new file mode 100644 index 00000000..bbb7c94f --- /dev/null +++ b/keepercli-package/src/keepercli/commands/share_management.py @@ -0,0 +1,1019 @@ +import argparse +import json +import math +import re +from enum import Enum +from typing import Optional + +from keepersdk import crypto, utils +from keepersdk.proto import folder_pb2, record_pb2 +from keepersdk.vault import vault_online, vault_utils, storage_types + +from . import base +from .. import api, prompt_utils, constants +from ..helpers import folder_utils, report_utils, share_utils +from ..params import KeeperParams + + +class ApiUrl(Enum): + SHARE_ADMIN = 'vault/am_i_share_admin' + SHARE_UPDATE = 'vault/records_share_update' + SHARE_FOLDER_UPDATE = 'vault/shared_folder_update_v3' + + +class ShareAction(Enum): + GRANT = 'grant' + REVOKE = 'revoke' + OWNER = 'owner' + CANCEL = 'cancel' + REMOVE = 'remove' + + +class ManagePermission(Enum): + ON = 'on' + OFF = 'off' + + +logger = api.get_logger() + + +def set_expiration_fields(obj, expiration): + """Set expiration and timerNotificationType fields on proto object if expiration is provided.""" + if isinstance(expiration, int): + if expiration > 0: + obj.expiration = expiration * 1000 + obj.timerNotificationType = record_pb2.NOTIFY_OWNER + elif expiration < 0: + obj.expiration = -1 + + +class ShareRecordCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='share-record', + description='Change the sharing permissions of an individual record', + ) + ShareRecordCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + + parser.add_argument( + '-e', '--email', dest='email', action='append', help='account email' + ) + parser.add_argument( + '--contacts-only', action='store_true', + help="Share only to known targets; Allows routing to alternate domains with matching usernames if needed" + ) + parser.add_argument( + '-f', '--force', action='store_true', help='Skip confirmation prompts' + ) + parser.add_argument( + '-a', '--action', dest='action', choices=[action.value for action in ShareAction], + default=ShareAction.GRANT.value, action='store', help='user share action. \'grant\' if omitted' + ) + parser.add_argument( + '-s', '--share', dest='can_share', action='store_true', help='can re-share record' + ) + parser.add_argument( + '-w', '--write', dest='can_edit', action='store_true', help='can modify record' + ) + parser.add_argument( + '-R', '--recursive', dest='recursive', action='store_true', + help='apply command to shared folder hierarchy' + ) + parser.add_argument( + '--dry-run', dest='dry_run', action='store_true', + help='display the permissions changes without committing them' + ) + expiration = parser.add_mutually_exclusive_group() + expiration.add_argument( + '--expire-at', dest='expire_at', action='store', help='share expiration: never or UTC datetime' + ) + expiration.add_argument( + '--expire-in', dest='expire_in', action='store', + metavar='[(mi)nutes|(h)ours|(d)ays|(mo)nths|(y)ears]', + help='share expiration: never or period' + ) + parser.add_argument( + 'record', nargs='?', type=str, action='store', help='record/shared folder path/UID' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + vault = context.vault + + uid_or_name = kwargs.get('record') + if not uid_or_name: + return self.get_parser().print_help() + + emails = kwargs.get('email') or [] + if not emails: + raise ValueError('share-record', '\'email\' parameter is missing') + + force = kwargs.get('force') + action = kwargs.get('action', ShareAction.GRANT.value) + contacts_only = kwargs.get('contacts_only') + dry_run = kwargs.get('dry_run') + can_edit = kwargs.get('can_edit') + can_share = kwargs.get('can_share') + recursive = kwargs.get('recursive') + + if contacts_only: + shared_objects = share_utils.get_share_objects(vault=vault) + known_users = shared_objects.get('users', {}) + known_emails = [u.casefold() for u in known_users.keys()] + is_unknown = lambda e: e.casefold() not in known_emails and utils.is_email(e) + unknowns = [e for e in emails if is_unknown(e)] + if unknowns: + username_map = {e: ShareRecordCommand.get_contact(e, known_users) for e in unknowns} + table = [[k, v] for k, v in username_map.items()] + logger.info(f'{len(unknowns)} unrecognized share recipient(s) and closest matching contact(s)') + report_utils.dump_report_data(table, ['Username', 'From Contacts']) + confirmed = force or prompt_utils.user_choice('\tReplace with known matching contact(s)?', 'yn', default='n') == 'y' + if confirmed: + good_emails = [e for e in emails if e not in unknowns] + replacements = [e for e in username_map.values() if e] + emails = [*good_emails, *replacements] + + if action == ShareAction.CANCEL.value: + ShareRecordCommand.cancel_share(vault, emails) + vault.sync_down() + return + else: + share_expiration = share_utils.get_share_expiration(kwargs.get('expire_at'), kwargs.get('expire_in')) + + request = ShareRecordCommand.prep_request( + context=context, + uid_or_name=uid_or_name, + emails=emails, + share_expiration=share_expiration, + action=action, + dry_run=dry_run or False, + can_edit=can_edit, + can_share=can_share, + recursive=recursive + ) + if request: + ShareRecordCommand.send_requests(vault, [request]) + + @staticmethod + def get_contact(user, contacts): + if not user or not contacts: + return None + + user_username = user.split('@')[0].casefold() + + for contact in contacts: + contact_username = contact.split('@')[0].casefold() + if user_username == contact_username: + return contact + + return None + + @staticmethod + def prep_request(context: KeeperParams, + emails: list[str], + action: str, + uid_or_name: str, + share_expiration: int, + dry_run: bool, + recursive: Optional[bool] = False, + can_edit: Optional[bool] = False, + can_share: Optional[bool] = False): + if not context or not hasattr(context, 'vault') or not context.vault or not hasattr(context.vault, 'vault_data') or not context.vault.vault_data: + raise ValueError("Vault or vault data is not initialized") + vault = context.vault + record_uid = None + folder_uid = None + shared_folder_uid = None + record_cache = {x.record_uid: x for x in vault.vault_data.records()} + shared_folder_cache = {x.shared_folder_uid: x for x in vault.vault_data.shared_folders()} + folder_cache = {x: x for x in getattr(vault.vault_data, '_folders', [])} + + if uid_or_name in record_cache: + record_uid = uid_or_name + elif uid_or_name in shared_folder_cache: + shared_folder_uid = uid_or_name + elif uid_or_name in folder_cache: + folder_uid = uid_or_name + else: + for sf_info in vault.vault_data.shared_folders(): + if uid_or_name == sf_info.name: + shared_folder_uid = sf_info.shared_folder_uid + break + + if shared_folder_uid is None and record_uid is None: + rs = folder_utils.try_resolve_path(context, uid_or_name) + if rs is not None: + folder, name = rs + if name: + for record in vault.vault_data.records(): + if record.title.lower() == name.lower(): + record_uid = record.record_uid + break + else: + # Handle shared folder types + if folder.folder_type == 'shared_folder': + folder_uid = folder.folder_uid + shared_folder_uid = folder_uid + elif folder.folder_type == 'shared_folder_folder': + folder_uid = folder.folder_uid + shared_folder_uid = folder.subfolders + + # Check share admin status + is_share_admin = False + if record_uid is None and folder_uid is None and shared_folder_uid is None: + if context._enterprise_loader: + try: + uid = utils.base64_url_decode(uid_or_name) + if isinstance(uid, bytes) and len(uid) == 16: + request = record_pb2.AmIShareAdmin() + obj_share_admin = record_pb2.IsObjectShareAdmin() + obj_share_admin.uid = uid + obj_share_admin.objectType = record_pb2.CHECK_SA_ON_RECORD + request.isObjectShareAdmin.append(obj_share_admin) + response = vault.keeper_auth.execute_auth_rest( + request=request, + response_type=record_pb2.AmIShareAdmin, + rest_endpoint=ApiUrl.SHARE_ADMIN.value + ) + if response and response.isObjectShareAdmin and response.isObjectShareAdmin[0].isAdmin: + is_share_admin = True + record_uid = uid_or_name + except Exception: + pass + + if record_uid is None and folder_uid is None and shared_folder_uid is None: + raise ValueError('share-record', 'Enter name or uid of existing record or shared folder') + + # Collect record UIDs + record_uids = set() + if record_uid: + record_uids.add(record_uid) + elif folder_uid: + folders = {folder_uid} + folder = vault.vault_data.get_folder(folder_uid) + if recursive and folder: + vault_utils.traverse_folder_tree( + vault=vault.vault_data, + folder=folder, + callback=lambda x: folders.add(x.folder_uid) + ) + record_uids = {uid for uid in folders if uid in record_cache} + elif shared_folder_uid: + if not recursive: + raise ValueError('share-record', '--recursive parameter is required') + if isinstance(shared_folder_uid, str): + sf = vault.vault_data.load_shared_folder(shared_folder_uid=shared_folder_uid) + if sf and sf.record_permissions: + record_uids.update(x.record_uid for x in sf.record_permissions) + elif isinstance(shared_folder_uid, list): + for sf_uid in shared_folder_uid: + if isinstance(sf_uid, str): + sf = vault.vault_data.load_shared_folder(shared_folder_uid=sf_uid) + if sf and sf.record_permissions: + record_uids.update(x.record_uid for x in sf.record_permissions) + + if not record_uids: + raise ValueError('share-record', 'There are no records to share selected') + + if action == 'owner' and len(emails) > 1: + raise ValueError('share-record', 'You can transfer ownership to a single account only') + + all_users = {email.casefold() for email in emails} + + # Handle user invitations and key loading + if not dry_run and action in (ShareAction.GRANT.value, ShareAction.OWNER.value): + invited = vault.keeper_auth.load_user_public_keys(list(all_users), send_invites=True) + if invited: + for email in invited: + logger.warning('Share invitation has been sent to \'%s\'', email) + logger.warning('Please repeat this command when invitation is accepted.') + all_users.difference_update(invited) + + if vault.keeper_auth._key_cache: + all_users.intersection_update(vault.keeper_auth._key_cache.keys()) + + if not all_users: + raise ValueError('share-record', 'Nothing to do.') + + # Load records in shared folders + if shared_folder_uid: + if isinstance(shared_folder_uid, str): + share_utils.load_records_in_shared_folder(vault=vault, shared_folder_uid=shared_folder_uid, record_uids=record_uids) + elif isinstance(shared_folder_uid, list): + for sf_uid in shared_folder_uid: + share_utils.load_records_in_shared_folder(vault=vault, shared_folder_uid=sf_uid, record_uids=record_uids) + + # Get share information for records not in cache + not_owned_records = {} if is_share_admin else None + share_info = share_utils.get_record_shares(vault=vault, record_uids=list(record_uids), is_share_admin=False) + if share_info and not_owned_records is not None: + for record_info in share_info: + record_uid = record_info.get('record_uid') + if record_uid: + not_owned_records[record_uid] = record_info + + # Build the request + rq = record_pb2.RecordShareUpdateRequest() + existing_shares = {} + record_titles = {} + transfer_ruids = set() + + for record_uid in record_uids: + # Get record data + if record_uid in record_cache: + rec = record_cache[record_uid] + elif not_owned_records and record_uid in not_owned_records: + rec = not_owned_records[record_uid] + elif is_share_admin: + rec = { + 'record_uid': record_uid, + 'shares': { + 'user_permissions': [{ + 'username': x, + 'owner': False, + 'share_admin': False, + 'shareable': action == 'revoke', + 'editable': action == 'revoke', + } for x in all_users] + } + } + else: + continue + + existing_shares.clear() + if isinstance(rec, dict): + if 'shares' in rec: + shares = rec['shares'] + if 'user_permissions' in shares: + for po in shares['user_permissions']: + existing_shares[po['username'].lower()] = po + del rec['shares'] + + if 'data_unencrypted' in rec: + try: + data = json.loads(rec['data_unencrypted'].decode()) + if isinstance(data, dict) and 'title' in data: + record_titles[record_uid] = data['title'] + except Exception: + pass + + record_path = share_utils.resolve_record_share_path(context=context, record_uid=record_uid) + + # Process each user + for email in all_users: + ro = record_pb2.SharedRecord() + ro.toUsername = email + ro.recordUid = utils.base64_url_decode(record_uid) + + if record_path: + if 'shared_folder_uid' in record_path: + ro.sharedFolderUid = utils.base64_url_decode(record_path['shared_folder_uid']) + if 'team_uid' in record_path: + ro.teamUid = utils.base64_url_decode(record_path['team_uid']) + + if action in {ShareAction.GRANT.value, ShareAction.OWNER.value}: + record_uid_to_use = rec.get('record_uid', record_uid) if isinstance(rec, dict) else getattr(rec, 'record_uid', record_uid) + record_key = vault.vault_data.get_record_key(record_uid=record_uid_to_use) + if record_key and email not in existing_shares and vault.keeper_auth._key_cache and email in vault.keeper_auth._key_cache: + keys = vault.keeper_auth._key_cache[email] + if vault.keeper_auth.auth_context.forbid_rsa and keys.ec: + ec_key = crypto.load_ec_public_key(keys.ec) + ro.recordKey = crypto.encrypt_ec(record_key, ec_key) + ro.useEccKey = True + elif not vault.keeper_auth.auth_context.forbid_rsa and keys.rsa: + rsa_key = crypto.load_rsa_public_key(keys.rsa) + ro.recordKey = crypto.encrypt_rsa(record_key, rsa_key) + ro.useEccKey = False + + if action == ShareAction.OWNER.value: + ro.transfer = True + transfer_ruids.add(record_uid) + else: + ro.editable = bool(can_edit) + ro.shareable = bool(can_share) + set_expiration_fields(ro, share_expiration) + elif email in existing_shares: + current = existing_shares[email] + if action == ShareAction.OWNER.value: + ro.transfer = True + transfer_ruids.add(record_uid) + else: + ro.editable = can_edit if can_edit is not None else current.get('editable') + ro.shareable = can_share if can_share is not None else current.get('shareable') + set_expiration_fields(ro, share_expiration) + + if email in existing_shares: + rq.updateSharedRecord.append(ro) + else: + rq.addSharedRecord.append(ro) + else: + if can_share or can_edit: + if email in existing_shares: + current = existing_shares[email] + ro.editable = False if can_edit else current.get('editable') + ro.shareable = False if can_share else current.get('shareable') + set_expiration_fields(ro, share_expiration) + rq.updateSharedRecord.append(ro) + else: + rq.removeSharedRecord.append(ro) + + return rq + + @staticmethod + def cancel_share(vault: vault_online.VaultOnline, emails: list[str]): + for email in emails: + request = { + 'command': 'cancel_share', + 'to_email': email + } + try: + vault.keeper_auth.execute_auth_command(request=request) + except Exception as e: + logger.warning(f'Failed to cancel share for {email}:{e}') + continue + vault.sync_down() + return + + @staticmethod + def send_requests(vault: vault_online.VaultOnline, requests): + MAX_BATCH_SIZE = 990 + STATUS_ATTRIBUTES = { + 'addSharedRecordStatus': ('granted to', 'grant'), + 'updateSharedRecordStatus': ('changed for', 'change'), + 'removeSharedRecordStatus': ('revoked from', 'revoke') + } + + def create_batch_request(request, max_size): + """Create a batch request by taking items from the source request.""" + batch = record_pb2.RecordShareUpdateRequest() + remaining = max_size + + # Process each record type in priority order + for attr_name in ['addSharedRecord', 'updateSharedRecord', 'removeSharedRecord']: + if remaining <= 0: + break + + source_list = getattr(request, attr_name) + if not source_list: + continue + + # Take items from the source list + items_to_take = min(remaining, len(source_list)) + target_list = getattr(batch, attr_name) + target_list.extend(source_list[:items_to_take]) + + # Remove taken items from source + del source_list[:items_to_take] + remaining -= items_to_take + + return batch + + def process_response_statuses(response): + """Process and log the status of each operation in the response.""" + for attr_name, (success_verb, failure_verb) in STATUS_ATTRIBUTES.items(): + if not hasattr(response, attr_name): + continue + + statuses = getattr(response, attr_name) + for status_record in statuses: + record_uid = utils.base64_url_encode(status_record.recordUid) + status = status_record.status + email = status_record.username + + if status == 'success': + logger.info( + 'Record "%s" access permissions has been %s user \'%s\'', + record_uid, success_verb, email + ) + else: + logger.info( + 'Failed to %s record "%s" access permissions for user \'%s\': %s', + failure_verb, record_uid, email, status_record.message + ) + + for request in requests: + # Process request in batches until all records are handled + while (len(request.addSharedRecord) > 0 or + len(request.updateSharedRecord) > 0 or + len(request.removeSharedRecord) > 0): + + # Create a batch request + batch_request = create_batch_request(request, MAX_BATCH_SIZE) + + # Send the batch request + response = vault.keeper_auth.execute_auth_rest( + rest_endpoint=ApiUrl.SHARE_UPDATE.value, + request=batch_request, + response_type=record_pb2.RecordShareUpdateResponse + ) + + process_response_statuses(response) + + +class ShareFolderCommand(base.ArgparseCommand): + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='share-folder', + description='Change the sharing permissions of shared folders' + ) + ShareFolderCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '-a', '--action', dest='action', choices=[ShareAction.GRANT.value, ShareAction.REMOVE.value], + default=ShareAction.GRANT.value, action='store', + help='shared folder action. \'grant\' if omitted' + ) + parser.add_argument( + '-e', '--email', dest='user', action='append', + help='account email, team, @existing for all users and teams in the folder, or \'*\' as default folder permission' + ) + parser.add_argument( + '-r', '--record', dest='record', action='append', + help='record name, record UID, @existing for all records in the folder, or \'*\' as default folder permission' + ) + parser.add_argument( + '-p', '--manage-records', dest='manage_records', action='store', + choices=[perm.value for perm in ManagePermission], help='account permission: can manage records.' + ) + parser.add_argument( + '-o', '--manage-users', dest='manage_users', action='store', + choices=[perm.value for perm in ManagePermission], help='account permission: can manage users.' + ) + parser.add_argument( + '-s', '--can-share', dest='can_share', action='store', + choices=[perm.value for perm in ManagePermission], help='record permission: can be shared' + ) + parser.add_argument( + '-d', '--can-edit', dest='can_edit', action='store', + choices=[perm.value for perm in ManagePermission], help='record permission: can be modified.' + ) + parser.add_argument( + '-f', '--force', dest='force', action='store_true', + help='Apply permission changes ignoring default folder permissions. Used on the initial sharing action' + ) + expiration = parser.add_mutually_exclusive_group() + expiration.add_argument( + '--expire-at', dest='expire_at', action='store', metavar='TIMESTAMP', + help='share expiration: never or ISO datetime (yyyy-MM-dd[ hh:mm:ss])' + ) + expiration.add_argument( + '--expire-in', dest='expire_in', action='store', metavar='PERIOD', + help='share expiration: never or period ([(y)ears|(mo)nths|(d)ays|(h)ours(mi)nutes]' + ) + parser.add_argument( + 'folder', nargs='+', type=str, action='store', help='shared folder path or UID' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError('Vault is not initialized.') + + vault = context.vault + + def get_share_admin_obj_uids(vault: vault_online.VaultOnline, obj_names, obj_type): + if not obj_names: + return None + try: + rq = record_pb2.AmIShareAdmin() + for name in obj_names: + try: + uid = utils.base64_url_decode(name) + if isinstance(uid, bytes) and len(uid) == 16: + osa = record_pb2.IsObjectShareAdmin() + osa.uid = uid + osa.objectType = obj_type + rq.isObjectShareAdmin.append(osa) + except: + pass + if len(rq.isObjectShareAdmin) > 0: + rs = vault.keeper_auth.execute_auth_rest(rest_endpoint=ApiUrl.SHARE_ADMIN.value, request=rq, response_type=record_pb2.AmIShareAdmin) + if rs and hasattr(rs, 'isObjectShareAdmin'): + sa_obj_uids = {sa_obj.uid for sa_obj in rs.isObjectShareAdmin if sa_obj.isAdmin} + sa_obj_uids = {utils.base64_url_encode(uid) for uid in sa_obj_uids} + return sa_obj_uids + except Exception as e: + raise ValueError(f'get_share_admin: msg = {e}') + + def get_folder_uids(context: KeeperParams, name: str) -> set[str]: + """Get folder UIDs by name or path.""" + folder_uids = set() + + if not context.vault or not context.vault.vault_data: + return folder_uids + + if name in context.vault.vault_data._folders: + folder_uids.add(name) + return folder_uids + + for folder in context.vault.vault_data.folders(): + if folder.name == name: + folder_uids.add(folder.folder_uid) + + if not folder_uids: + try: + folder, _ = folder_utils.try_resolve_path(context, name) + if folder: + folder_uids.add(folder.folder_uid) + except: + pass + + return folder_uids + + def get_record_uids(context: KeeperParams, name: str) -> set[str]: + """Get record UIDs by name or UID.""" + record_uids = set() + + if not context.vault or not context.vault.vault_data: + return record_uids + + record = context.vault.vault_data.get_record(name) + if record: + record_uids.add(name) + return record_uids + + for record_info in context.vault.vault_data.records(): + if record_info.title == name: + record_uids.add(record_info.record_uid) + + return record_uids + + names = kwargs.get('folder') + if not isinstance(names, list): + names = [names] + + all_folders = any(True for x in names if x == '*') + if all_folders: + names = [x for x in names if x != '*'] + + shared_folder_cache = {x.shared_folder_uid: x for x in vault.vault_data.shared_folders()} + folder_cache = {x.folder_uid: x for x in vault.vault_data.folders()} + shared_folder_uids = set() + if all_folders: + shared_folder_uids.update(shared_folder_cache.keys()) + else: + get_folder_by_uid = lambda uid: folder_cache.get(uid) + folder_uids = { + uid + for name in names if name + for uid in get_folder_uids(context, name) + } + folders = {get_folder_by_uid(uid) for uid in folder_uids if get_folder_by_uid(uid)} + shared_folder_uids.update([uid for uid in folder_uids if uid in shared_folder_cache]) + + sf_subfolders = {f for f in folders if f and f.folder_type == 'shared_folder_folder'} + shared_folder_uids.update({f.folder_scope_uid for f in sf_subfolders if f.folder_scope_uid}) + + unresolved_names = [name for name in names if name and not get_folder_uids(context, name)] + share_admin_folder_uids = get_share_admin_obj_uids(vault=vault, obj_names=unresolved_names, obj_type=record_pb2.CHECK_SA_ON_SF) + shared_folder_uids.update(share_admin_folder_uids or []) + + if not shared_folder_uids: + raise ValueError('share-folder', 'Enter name of at least one existing folder') + + action = kwargs.get('action') or ShareAction.GRANT.value + + share_expiration = None + if action == ShareAction.GRANT.value: + share_expiration = share_utils.get_share_expiration(kwargs.get('expire_at'), kwargs.get('expire_in')) + + as_users = set() + as_teams = set() + + all_users = False + default_account = False + if 'user' in kwargs: + for u in (kwargs.get('user') or []): + if u == '*': + default_account = True + elif u in ('@existing', '@current'): + all_users = True + else: + em = re.match(constants.EMAIL_PATTERN, u) + if em is not None: + as_users.add(u.lower()) + else: + teams = share_utils.get_share_objects(vault=vault).get('teams', {}) + teams_map = {uid: team.get('name') for uid, team in teams.items()} + if len(teams) >= 500: + teams = vault_utils.load_available_teams(auth=vault.keeper_auth) + teams_map.update({t.team_uid: t.name for t in teams}) + + matches = [uid for uid, name in teams_map.items() if u in (name, uid)] + if len(matches) != 1: + logger.warning(f'User "{u}" could not be resolved as email or team' if not matches + else f'Multiple matches were found for team "{u}". Try using its UID -- which can be found via `list-team` -- instead') + else: + [team] = matches + as_teams.add(team) + + record_uids = set() + all_records = False + default_record = False + unresolved_names = [] + if 'record' in kwargs: + records = kwargs.get('record') or [] + for r in records: + if r == '*': + default_record = True + elif r in ('@existing', '@current'): + all_records = True + else: + r_uids = get_record_uids(context, r) + record_uids.update(r_uids) if r_uids else unresolved_names.append(r) + + if unresolved_names: + sa_record_uids = get_share_admin_obj_uids(vault=vault, obj_names=unresolved_names, obj_type=record_pb2.CHECK_SA_ON_RECORD) + record_uids.update(sa_record_uids or {}) + + if len(as_users) == 0 and len(as_teams) == 0 and len(record_uids) == 0 and \ + not default_record and not default_account and \ + not all_users and not all_records: + logger.info('Nothing to do') + return + + rq_groups = [] + + def prep_rq(recs, users, curr_sf): + return self.prepare_request(vault, kwargs, curr_sf, users, sf_teams, recs, default_record=default_record, + default_account=default_account, share_expiration=share_expiration) + + for sf_uid in shared_folder_uids: + sf_users = as_users.copy() + sf_teams = as_teams.copy() + sf_records = record_uids.copy() + + if sf_uid in shared_folder_cache: + sh_fol = vault.vault_data.load_shared_folder(sf_uid) + if (all_users or all_records) and sh_fol: + if all_users: + if sh_fol.user_permissions: + sf_users.update((x.name for x in sh_fol.user_permissions if x.name != context.username)) + if all_records: + if sh_fol and sh_fol.record_permissions: + sf_records.update((x.record_uid for x in sh_fol.record_permissions)) + else: + sh_fol = { + 'shared_folder_uid': sf_uid, + 'users': [{'username': x, 'manage_records': action != ShareAction.GRANT.value, 'manage_users': action != ShareAction.GRANT.value} + for x in as_users], + 'teams': [{'team_uid': x, 'manage_records': action != ShareAction.GRANT.value, 'manage_users': action != ShareAction.GRANT.value} + for x in as_teams], + 'records': [{'record_uid': x, 'can_share': action != ShareAction.GRANT.value, 'can_edit': action != ShareAction.GRANT.value} + for x in record_uids] + } + chunk_size = 500 + rec_list = list(sf_records) + user_list = list(sf_users) + num_rec_chunks = math.ceil(len(sf_records) / chunk_size) + num_user_chunks = math.ceil(len(sf_users) / chunk_size) + num_rq_groups = num_user_chunks or 1 * num_rec_chunks or 1 + while len(rq_groups) < num_rq_groups: + rq_groups.append([]) + rec_chunks = [rec_list[i * chunk_size:(i + 1) * chunk_size] for i in range(num_rec_chunks)] or [[]] + user_chunks = [user_list[i * chunk_size:(i + 1) * chunk_size] for i in range(num_user_chunks)] or [[]] + group_idx = 0 + shared_folder_revision = vault.vault_data.storage.shared_folders.get_entity(sf_uid).revision + sf_unencrypted_key = vault.vault_data.get_shared_folder_key(shared_folder_uid=sh_fol.shared_folder_uid) + for r_chunk in rec_chunks: + for u_chunk in user_chunks: + sf_info = sh_fol.copy() if isinstance(sh_fol, dict) else { + 'shared_folder_uid': sf_uid, + 'users': sh_fol.user_permissions, + 'teams': [], + 'records': sh_fol.record_permissions, + 'shared_folder_key_unencrypted': sf_unencrypted_key, + 'default_manage_users': sh_fol.default_can_share, + 'default_manage_records': sh_fol.default_can_edit, + 'revision': shared_folder_revision + } + if group_idx and isinstance(sf_info, dict) and 'revision' in sf_info: + del sf_info['revision'] + rq_groups[group_idx].append(prep_rq(r_chunk, u_chunk, sf_info)) + group_idx += 1 + self.send_requests(vault=vault, partitioned_requests=rq_groups) + + @staticmethod + def prepare_request(vault: vault_online.VaultOnline, kwargs, curr_sf, users, teams, rec_uids, *, + default_record=False, default_account=False, + share_expiration=None): + rq = folder_pb2.SharedFolderUpdateV3Request() + rq.sharedFolderUid = utils.base64_url_decode(curr_sf['shared_folder_uid']) + if 'revision' in curr_sf: + rq.revision = curr_sf['revision'] + else: + rq.forceUpdate = True + action = kwargs.get('action') or ShareAction.GRANT.value + mr = kwargs.get('manage_records') + mu = kwargs.get('manage_users') + if default_account and action == ShareAction.GRANT.value: + if mr is not None: + rq.defaultManageRecords = folder_pb2.BOOLEAN_TRUE if mr == 'on' else folder_pb2.BOOLEAN_FALSE + else: + rq.defaultManageRecords = folder_pb2.BOOLEAN_NO_CHANGE + if mu is not None: + rq.defaultManageUsers = folder_pb2.BOOLEAN_TRUE if mu == 'on' else folder_pb2.BOOLEAN_FALSE + else: + rq.defaultManageUsers = folder_pb2.BOOLEAN_NO_CHANGE + + if len(users) > 0: + existing_users = {x['username'] if isinstance(x, dict) else x.name for x in curr_sf.get('users', [])} + for email in users: + uo = folder_pb2.SharedFolderUpdateUser() + uo.username = email + set_expiration_fields(uo, share_expiration) + if email in existing_users: + if action == ShareAction.GRANT.value: + uo.manageRecords = folder_pb2.BOOLEAN_NO_CHANGE if mr is None else folder_pb2.BOOLEAN_TRUE if mr == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + uo.manageUsers = folder_pb2.BOOLEAN_NO_CHANGE if mu is None else folder_pb2.BOOLEAN_TRUE if mu == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + rq.sharedFolderUpdateUser.append(uo) + elif action == ShareAction.REMOVE.value: + rq.sharedFolderRemoveUser.append(uo.username) + elif action == ShareAction.GRANT.value: + invited = vault.keeper_auth.load_user_public_keys([email], send_invites=True) + if invited: + for username in invited: + logger.warning('Share invitation has been sent to \'%s\'', username) + logger.warning('Please repeat this command when invitation is accepted.') + keys = vault.keeper_auth._key_cache.get(email) if vault.keeper_auth._key_cache else None + if keys and (keys.rsa or keys.ec): + uo.manageRecords = folder_pb2.BOOLEAN_TRUE if curr_sf.get('default_manage_records') is True and mr is None else folder_pb2.BOOLEAN_TRUE if mr == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + uo.manageUsers = folder_pb2.BOOLEAN_TRUE if curr_sf.get('default_manage_users') is True and mu is None else folder_pb2.BOOLEAN_TRUE if mu == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + sf_key = curr_sf.get('shared_folder_key_unencrypted') + if sf_key: + if vault.keeper_auth.auth_context.forbid_rsa and keys.ec: + ec_key = crypto.load_ec_public_key(keys.ec) + uo.typedSharedFolderKey.encryptedKey = crypto.encrypt_ec(sf_key, ec_key) + uo.typedSharedFolderKey.encryptedKeyType = folder_pb2.encrypted_by_public_key_ecc + elif not vault.keeper_auth.auth_context.forbid_rsa and keys.rsa: + rsa_key = crypto.load_rsa_public_key(keys.rsa) + uo.typedSharedFolderKey.encryptedKey = crypto.encrypt_rsa(sf_key, rsa_key) + uo.typedSharedFolderKey.encryptedKeyType = folder_pb2.encrypted_by_public_key + + rq.sharedFolderAddUser.append(uo) + else: + logger.warning('User %s not found', email) + + if len(teams) > 0: + existing_teams = {x['team_uid']: x for x in curr_sf.get('teams', [])} + for team_uid in teams: + to = folder_pb2.SharedFolderUpdateTeam() + to.teamUid = utils.base64_url_decode(team_uid) + set_expiration_fields(to, share_expiration) + if team_uid in existing_teams: + team = existing_teams[team_uid] + if action == ShareAction.GRANT.value: + to.manageRecords = team.get('manage_records') is True if mr is None else mr == ManagePermission.ON.value + to.manageUsers = team.get('manage_users') is True if mu is None else mu == ManagePermission.ON.value + rq.sharedFolderUpdateTeam.append(to) + elif action == ShareAction.REMOVE.value: + rq.sharedFolderRemoveTeam.append(to.teamUid) + elif action == ShareAction.GRANT.value: + to.manageRecords = True if mr else curr_sf.get('default_manage_records') is True + to.manageUsers = True if mu else curr_sf.get('default_manage_users') is True + team_sf_key = curr_sf.get('shared_folder_key_unencrypted') # type: Optional[bytes] + if team_sf_key: + vault.keeper_auth.load_team_keys([team_uid]) + keys = vault.keeper_auth._key_cache.get(team_uid) if vault.keeper_auth._key_cache else None + if keys: + if keys.aes: + if vault.keeper_auth.auth_context.forbid_rsa: + to.typedSharedFolderKey.encryptedKey = crypto.encrypt_aes_v2(team_sf_key, keys.aes) + to.typedSharedFolderKey.encryptedKeyType = folder_pb2.encrypted_by_data_key_gcm + else: + to.typedSharedFolderKey.encryptedKey = crypto.encrypt_aes_v1(team_sf_key, keys.aes) + to.typedSharedFolderKey.encryptedKeyType = folder_pb2.encrypted_by_data_key + elif vault.keeper_auth.auth_context.forbid_rsa and keys.ec: + ec_key = crypto.load_ec_public_key(keys.ec) + to.typedSharedFolderKey.encryptedKey = crypto.encrypt_ec(team_sf_key, ec_key) + to.typedSharedFolderKey.encryptedKeyType = folder_pb2.encrypted_by_public_key_ecc + elif not vault.keeper_auth.auth_context.forbid_rsa and keys.rsa: + rsa_key = crypto.load_rsa_public_key(keys.rsa) + to.typedSharedFolderKey.encryptedKey = crypto.encrypt_rsa(team_sf_key, rsa_key) + to.typedSharedFolderKey.encryptedKeyType = folder_pb2.encrypted_by_public_key + else: + continue + else: + continue + else: + logger.info('Shared folder key is not available.') + rq.sharedFolderAddTeam.append(to) + + ce = kwargs.get('can_edit') + cs = kwargs.get('can_share') + + if default_record and action == ShareAction.GRANT.value: + rq.defaultCanEdit = folder_pb2.BOOLEAN_NO_CHANGE if ce is None else folder_pb2.BOOLEAN_TRUE if ce == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + rq.defaultCanShare = folder_pb2.BOOLEAN_NO_CHANGE if cs is None else folder_pb2.BOOLEAN_TRUE if cs == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + + if len(rec_uids) > 0: + existing_records = {x['record_uid'] for x in curr_sf.get('records', [])} + for record_uid in rec_uids: + ro = folder_pb2.SharedFolderUpdateRecord() + ro.recordUid = utils.base64_url_decode(record_uid) + set_expiration_fields(ro, share_expiration) + + if record_uid in existing_records: + if action == ShareAction.GRANT.value: + ro.canEdit = folder_pb2.BOOLEAN_NO_CHANGE if ce is None else folder_pb2.BOOLEAN_TRUE if ce == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + ro.canShare = folder_pb2.BOOLEAN_NO_CHANGE if cs is None else folder_pb2.BOOLEAN_TRUE if cs == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + rq.sharedFolderUpdateRecord.append(ro) + elif action == ShareAction.REMOVE.value: + rq.sharedFolderRemoveRecord.append(ro.recordUid) + else: + if action == ShareAction.GRANT.value: + ro.canEdit = folder_pb2.BOOLEAN_TRUE if curr_sf.get('default_can_edit') is True and ce is None else folder_pb2.BOOLEAN_TRUE if ce == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + ro.canShare = folder_pb2.BOOLEAN_TRUE if curr_sf.get('default_can_share') is True and cs is None else folder_pb2.BOOLEAN_TRUE if cs == ManagePermission.ON.value else folder_pb2.BOOLEAN_FALSE + sf_key = curr_sf.get('shared_folder_key_unencrypted') + if sf_key: + rec = vault.vault_data.get_record(record_uid) + if rec: + rec_key = vault.vault_data.get_record_key(record_uid) + if rec_key: + if rec.version < 3: + ro.encryptedRecordKey = crypto.encrypt_aes_v1(rec_key, sf_key) + else: + ro.encryptedRecordKey = crypto.encrypt_aes_v2(rec_key, sf_key) + rq.sharedFolderAddRecord.append(ro) + return rq + + @staticmethod + def send_requests(vault:vault_online.VaultOnline, partitioned_requests): + for requests in partitioned_requests: + while requests: + vault.auto_sync = True + chunk = requests[:999] + requests = requests[999:] + rqs = folder_pb2.SharedFolderUpdateV3RequestV2() + rqs.sharedFoldersUpdateV3.extend(chunk) + try: + rss = vault.keeper_auth.execute_auth_rest(rest_endpoint=ApiUrl.SHARE_FOLDER_UPDATE.value, request=rqs, response_type=folder_pb2.SharedFolderUpdateV3ResponseV2, payload_version=1) + if rss and hasattr(rss, 'sharedFoldersUpdateV3Response'): + for rs in rss.sharedFoldersUpdateV3Response: + team_cache = vault.vault_data.teams() + for attr in ( + 'sharedFolderAddTeamStatus', 'sharedFolderUpdateTeamStatus', + 'sharedFolderRemoveTeamStatus'): + if hasattr(rs, attr): + statuses = getattr(rs, attr) + for t in statuses: + team_uid = utils.base64_url_encode(t.teamUid) + team = next((x for x in team_cache if x.team_uid == team_uid), None) + if team: + status = t.status + if status == 'success': + logger.info('Team share \'%s\' %s', team.name, + 'added' if attr == 'sharedFolderAddTeamStatus' else + 'updated' if attr == 'sharedFolderUpdateTeamStatus' else + 'removed') + else: + logger.warning('Team share \'%s\' failed', team.name) + + for attr in ( + 'sharedFolderAddUserStatus', 'sharedFolderUpdateUserStatus', + 'sharedFolderRemoveUserStatus'): + if hasattr(rs, attr): + statuses = getattr(rs, attr) + for s in statuses: + username = s.username + status = s.status + if status == 'success': + logger.info('User share \'%s\' %s', username, + 'added' if attr == 'sharedFolderAddUserStatus' else + 'updated' if attr == 'sharedFolderUpdateUserStatus' else + 'removed') + elif status == 'invited': + logger.info('User \'%s\' invited', username) + else: + logger.warning('User share \'%s\' failed', username) + + for attr in ('sharedFolderAddRecordStatus', 'sharedFolderUpdateRecordStatus', + 'sharedFolderRemoveRecordStatus'): + if hasattr(rs, attr): + statuses = getattr(rs, attr) + for r in statuses: + record_uid = utils.base64_url_encode(r.recordUid) + status = r.status + if record_uid in vault.vault_data._records: + rec = vault.vault_data.get_record(record_uid) + title = rec.title if rec else record_uid + else: + title = record_uid + if status == 'success': + logger.info('Record share \'%s\' %s', title, + 'added' if attr == 'sharedFolderAddRecordStatus' else + 'updated' if attr == 'sharedFolderUpdateRecordStatus' else + 'removed') + else: + logger.warning('Record share \'%s\' failed', title) + except Exception as kae: + logger.error(kae) + return diff --git a/keepercli-package/src/keepercli/helpers/share_utils.py b/keepercli-package/src/keepercli/helpers/share_utils.py new file mode 100644 index 00000000..198a729f --- /dev/null +++ b/keepercli-package/src/keepercli/helpers/share_utils.py @@ -0,0 +1,439 @@ +import datetime +import itertools +from typing import Optional, Dict, List, Any, Generator, Tuple, Iterable + +from keepersdk import crypto, utils +from keepersdk.proto import record_pb2 +from keepersdk.vault import storage_types, vault_online, vault_record + +from .. import api +from ..commands import enterprise_utils +from ..helpers import timeout_utils +from ..params import KeeperParams + + +RECORD_DETAILS_URL = 'vault/get_records_details' +SHARE_OBJECTS_API = 'vault/get_share_objects' + + +logger = api.get_logger() + + +def get_share_expiration(expire_at: Optional[str], expire_in: Optional[str]) -> int: + if not expire_at and not expire_in: + return 0 + + dt = None + if isinstance(expire_at, str): + if expire_at == 'never': + return -1 + dt = datetime.datetime.fromisoformat(expire_at) + elif isinstance(expire_in, str): + if expire_in == 'never': + return -1 + td = timeout_utils.parse_timeout(expire_in) + dt = datetime.datetime.now() + td + if dt is None: + raise ValueError(f'Incorrect expiration: {expire_at or expire_in}') + + return int(dt.timestamp()) + + +def get_share_objects(vault: vault_online.VaultOnline) -> Dict[str, Dict[str, Any]]: + request = record_pb2.GetShareObjectsRequest() + + response = vault.keeper_auth.execute_auth_rest( + rest_endpoint=SHARE_OBJECTS_API, + request=request, + response_type=record_pb2.GetShareObjectsResponse + ) + + if not response: + return {'users': {}, 'enterprises': {}, 'teams': {}} + + users_by_type = { + 'relationship': response.shareRelationships, + 'family': response.shareFamilyUsers, + 'enterprise': response.shareEnterpriseUsers, + 'mc': response.shareMCEnterpriseUsers, + } + + def process_users(users_data: Iterable[Any], category: str) -> Dict[str, Dict[str, Any]]: + """Process user data and add category information.""" + return { + user.username: { + 'name': user.fullname, + 'is_sa': user.isShareAdmin, + 'enterprise_id': user.enterpriseId, + 'status': user.status, + 'category': category + } for user in users_data + } + + users = {} + for category, users_data in users_by_type.items(): + users.update(process_users(users_data, category)) + + enterprises = { + str(enterprise.enterpriseId): enterprise.enterprisename + for enterprise in response.shareEnterpriseNames + } + + def process_teams(teams_data: Iterable[Any]) -> Dict[str, Dict[str, Any]]: + return { + utils.base64_url_encode(team.teamUid): { + 'name': team.teamname, + 'enterprise_id': team.enterpriseId + } for team in teams_data + } + + teams = process_teams(response.shareTeams) + teams_mc = process_teams(response.shareMCTeams) + + return { + 'users': users, + 'enterprises': enterprises, + 'teams': {**teams, **teams_mc} + } + + +def load_records_in_shared_folder( + vault: vault_online.VaultOnline, + shared_folder_uid: str, + record_uids: Optional[set[str]] = None +) -> None: + shared_folder = None + for shared_folder_info in vault.vault_data.shared_folders(): + if shared_folder_uid == shared_folder_info.shared_folder_uid: + shared_folder = vault.vault_data.load_shared_folder(shared_folder_uid=shared_folder_uid) + break + + if not shared_folder: + raise Exception(f'Shared folder "{shared_folder_uid}" is not loaded.') + + shared_folder_key = vault.vault_data._shared_folders[shared_folder_uid].shared_folder_key + record_keys = {} + sf_record_keys = vault.vault_data.storage.record_keys.get_links_by_object(shared_folder.shared_folder_uid) or [] + for rk in sf_record_keys: + record_uid = getattr(rk, 'record_uid', None) + try: + key = utils.base64_url_decode( + str(getattr(rk, 'record_key', b''), 'utf-8') + if isinstance(getattr(rk, 'record_key', b''), bytes) + else getattr(rk, 'record_key', '') + ) + if len(key) == 60: + record_key = crypto.decrypt_aes_v2(key, shared_folder_key) + else: + record_key = crypto.decrypt_aes_v1(key, shared_folder_key) + record_keys[record_uid] = record_key + except Exception as e: + logger.error(f'Cannot decrypt record "{record_uid}" key: {e}') + + record_cache = [x.record_uid for x in vault.vault_data.records()] + + if record_uids: + record_set = set(record_uids) + record_set.intersection_update(record_keys.keys()) + else: + record_set = set(record_keys.keys()) + record_set.difference_update(record_cache) + + # Load records in batches + while len(record_set) > 0: + rq = record_pb2.GetRecordDataWithAccessInfoRequest() + rq.clientTime = utils.current_milli_time() + rq.recordDetailsInclude = record_pb2.DATA_PLUS_SHARE + + for uid in record_set: + try: + rq.recordUid.append(utils.base64_url_decode(uid)) + except Exception as e: + logger.debug('Incorrect record UID "%s": %s', uid, e) + record_set.clear() + + rs = vault.keeper_auth.execute_auth_rest( + rest_endpoint=RECORD_DETAILS_URL, + request=rq, + response_type=record_pb2.GetRecordDataWithAccessInfoResponse + ) + + if not rs or not rs.recordDataWithAccessInfo: + logger.warning("No record data received from API") + break + + for record_info in rs.recordDataWithAccessInfo: + record_uid = utils.base64_url_encode(record_info.recordUid) + record_data = record_info.recordData + try: + if record_data.recordUid and record_data.recordKey: + owner_id = utils.base64_url_encode(record_data.recordUid) + if owner_id in record_keys: + record_keys[record_uid] = crypto.decrypt_aes_v2(record_data.recordKey, record_keys[owner_id]) + + if record_uid not in record_keys: + continue + + record_key = record_keys[record_uid] + version = record_data.version + record = { + 'record_uid': record_uid, + 'revision': record_data.revision, + 'version': version, + 'shared': record_data.shared, + 'data': record_data.encryptedRecordData, + 'record_key_unencrypted': record_keys[record_uid], + 'client_modified_time': record_data.clientModifiedTime, + } + data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) + if version <= 2: + record['data_unencrypted'] = crypto.decrypt_aes_v1(data_decoded, record_key) + else: + record['data_unencrypted'] = crypto.decrypt_aes_v2(data_decoded, record_key) + + # Handle extra data for v2 records + if record_data.encryptedExtraData and version <= 2: + record['extra'] = record_data.encryptedExtraData + extra_decoded = utils.base64_url_decode(record_data.encryptedExtraData) + record['extra_unencrypted'] = crypto.decrypt_aes_v1(extra_decoded, record_key) + + # Handle v3 typed records with references + if version == 3: + v3_record = vault.vault_data.load_record(record_uid=record_uid) + if isinstance(v3_record, vault_record.TypedRecord): + for ref in itertools.chain(v3_record.fields, v3_record.custom): + if ref.type.endswith('Ref') and isinstance(ref.value, list): + record_set.update(ref.value) + + # Handle v4 records with file attachments + elif version == 4: + if record_data.fileSize > 0: + record['file_size'] = record_data.fileSize + if record_data.thumbnailSize > 0: + record['thumbnail_size'] = record_data.thumbnailSize + + # Handle linked record metadata + if record_data.recordUid and record_data.recordKey: + record['owner_uid'] = utils.base64_url_encode(record_data.recordUid) + record['link_key'] = utils.base64_url_encode(record_data.recordKey) + + # Add share permissions + record['shares'] = { + 'user_permissions': [{ + 'username': up.username, + 'owner': up.owner, + 'share_admin': up.shareAdmin, + 'shareable': up.sharable, + 'editable': up.editable, + 'awaiting_approval': up.awaitingApproval, + 'expiration': up.expiration, + } for up in record_info.userPermission], + 'shared_folder_permissions': [{ + 'shared_folder_uid': utils.base64_url_encode(sp.sharedFolderUid), + 'reshareable': sp.resharable, + 'editable': sp.editable, + 'revision': sp.revision, + 'expiration': sp.expiration, + } for sp in record_info.sharedFolderPermission], + } + record_set.add(record_uid) + except Exception as e: + logger.debug('Error decrypting record "%s": %s', record_uid, e) + + +def get_record_shares( + vault: vault_online.VaultOnline, + record_uids: List[str], + is_share_admin: bool = False +) -> Optional[List[Dict[str, Any]]]: + record_cache = {x.record_uid: x for x in vault.vault_data.records()} + + def needs_share_info(uid: str) -> bool: + """Check if a record needs share information.""" + if uid in record_cache: + record = record_cache[uid] + return not hasattr(record, 'shares') + return is_share_admin + + def create_record_info(record_uid: str, keeper_record: Optional[Any] = None) -> Dict[str, Any]: + """Create basic record information dictionary.""" + rec = {'record_uid': record_uid} + + if keeper_record: + if hasattr(keeper_record, 'title'): + rec['title'] = keeper_record.title + if hasattr(keeper_record, 'data_unencrypted'): + rec['data_unencrypted'] = keeper_record.data_unencrypted + + return rec + + def process_user_permissions(info: Any) -> List[Dict[str, Any]]: + """Process user permissions from record info.""" + user_permissions = [] + for up in info.userPermission: + permission = { + 'username': up.username, + 'owner': up.owner, + 'share_admin': up.shareAdmin, + 'shareable': up.sharable, + 'editable': up.editable, + } + if up.awaitingApproval: + permission['awaiting_approval'] = up.awaitingApproval + if up.expiration > 0: + permission['expiration'] = str(up.expiration) + user_permissions.append(permission) + return user_permissions + + def process_shared_folder_permissions(info: Any) -> List[Dict[str, Any]]: + """Process shared folder permissions from record info.""" + shared_folder_permissions = [] + for sp in info.sharedFolderPermission: + permission = { + 'shared_folder_uid': utils.base64_url_encode(sp.sharedFolderUid), + 'reshareable': sp.resharable, + 'editable': sp.editable, + 'revision': sp.revision, + } + if sp.expiration > 0: + permission['expiration'] = sp.expiration + shared_folder_permissions.append(permission) + return shared_folder_permissions + + uids_needing_info = [uid for uid in record_uids if needs_share_info(uid)] + + if not uids_needing_info: + return None + + result = [] + try: + chunk_size = 999 + for i in range(0, len(uids_needing_info), chunk_size): + chunk = uids_needing_info[i:i + chunk_size] + + request = record_pb2.GetRecordDataWithAccessInfoRequest() + request.clientTime = utils.current_milli_time() + request.recordUid.extend([utils.base64_url_decode(uid) for uid in chunk]) + request.recordDetailsInclude = record_pb2.SHARE_ONLY + + response = vault.keeper_auth.execute_auth_rest( + rest_endpoint=RECORD_DETAILS_URL, + request=request, + response_type=record_pb2.GetRecordDataWithAccessInfoResponse + ) + + if not response or not response.recordDataWithAccessInfo: + logger.error("No response or missing recordDataWithAccessInfo from Keeper API.") + continue + + for info in response.recordDataWithAccessInfo: + record_uid = utils.base64_url_encode(info.recordUid) + + rec = create_record_info(record_uid) + + if isinstance(rec, dict): + rec['shares'] = { + 'user_permissions': process_user_permissions(info), + 'shared_folder_permissions': process_shared_folder_permissions(info) + } + + result.append(rec) + + except Exception as e: + logger.error(f"Error fetching record shares: {e}") + + return result if result else None + + +def resolve_record_share_path(context: KeeperParams, record_uid: str) -> Optional[Dict[str, str]]: + return resolve_record_permission_path(context=context, record_uid=record_uid, permission='can_share') + + +def resolve_record_permission_path( + context: KeeperParams, + record_uid: str, + permission: str +) -> Optional[Dict[str, str]]: + for ap in enumerate_record_access_paths(context=context, record_uid=record_uid): + if ap.get(permission): + path = { + 'record_uid': record_uid + } + if 'shared_folder_uid' in ap: + path['shared_folder_uid'] = ap['shared_folder_uid'] + if 'team_uid' in ap: + path['team_uid'] = ap['team_uid'] + return path + + return None + + +def enumerate_record_access_paths( + context: KeeperParams, + record_uid: str +) -> Generator[Dict[str, Any], None, None]: + + def create_access_path( + shared_folder_uid: str, + can_edit: bool, + can_share: bool, + team_uid: Optional[str] = None + ) -> Dict[str, Any]: + """Create a standardized access path dictionary.""" + path = { + 'record_uid': record_uid, + 'shared_folder_uid': shared_folder_uid, + 'can_edit': can_edit, + 'can_share': can_share, + 'can_view': True + } + if team_uid: + path['team_uid'] = team_uid + return path + + def process_team_permissions( + shared_folder: Any, + base_can_edit: bool, + base_can_share: bool + ) -> Generator[Dict[str, Any], None, None]: + """Process team-based permissions for a shared folder.""" + if not context.enterprise_data: + return + + for user_permission in shared_folder.user_permissions: + if user_permission.user_type != storage_types.SharedFolderUserType.Team: + continue + + team_uid = user_permission.user_uid + team = enterprise_utils.TeamUtils.resolve_single_team( + context.enterprise_data, team_uid + ) + + if team: + yield create_access_path( + shared_folder_uid=shared_folder.shared_folder_uid, + can_edit=base_can_edit and not team.restrict_edit, + can_share=base_can_share and not team.restrict_share, + team_uid=team_uid + ) + + for shared_folder_info in context.vault.vault_data.shared_folders(): + shared_folder_uid = shared_folder_info.shared_folder_uid + + shared_folder = context.vault.vault_data.load_shared_folder( + shared_folder_uid=shared_folder_uid + ) + + is_owner = context.vault.vault_data.get_record(record_uid).flags == vault_record.RecordFlags.IsOwner + + can_edit, can_share = is_owner, is_owner + + if hasattr(shared_folder, 'key_type'): + yield create_access_path( + shared_folder_uid=shared_folder_uid, + can_edit=can_edit, + can_share=can_share + ) + else: + yield from process_team_permissions(shared_folder, can_edit, can_share) + diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 0b959732..033f08c7 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -24,7 +24,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, - record_type, secrets_manager) + record_type, secrets_manager, share_management) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -52,6 +52,8 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('load-record-types', record_type.LoadRecordTypesCommand(), base.CommandScope.Vault) commands.register_command('download-record-types', record_type.DownloadRecordTypesCommand(), base.CommandScope.Vault) commands.register_command('secrets-manager-app', secrets_manager.SecretsManagerAppCommand(), base.CommandScope.Vault) + commands.register_command('share-record', share_management.ShareRecordCommand(), base.CommandScope.Vault, 'sr') + commands.register_command('share-folder', share_management.ShareFolderCommand(), base.CommandScope.Vault, 'sf') if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/authentication/yubikey.py b/keepersdk-package/src/keepersdk/authentication/yubikey.py index bedf9280..4ff6e7bd 100644 --- a/keepersdk-package/src/keepersdk/authentication/yubikey.py +++ b/keepersdk-package/src/keepersdk/authentication/yubikey.py @@ -28,13 +28,11 @@ def yubikey_authenticate(request: Dict[str, Any], user_interaction: UserInteract origin = '' options = request['publicKeyCredentialRequestOptions'] - if 'extensions' not in options: - options['extensions'] = {} - if 'largeBlob' not in options['extensions']: - options['extensions']['largeBlob'] = {'read': False} if 'extensions' in options: extensions = options['extensions'] origin = extensions.get('appid') or '' + if 'largeBlob' not in options['extensions']: + options['extensions']['largeBlob'] = {'read': None} credentials = options.get('allowCredentials') or [] for c in credentials: diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index b72628a1..2e6eba0c 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -93,7 +93,7 @@ def create_secrets_manager_app(vault: vault_online.VaultOnline, name: str, force existing_app = next((r for r in vault.vault_data.records() if r.title == name), None) if existing_app and not force_add: - raise ValueError(f'Application with the same name {name} already exists.') + raise ValueError(f'Application with the same name {name} already exists. Set force to true to add Application with same name') app_record_data = { 'title': name, From 5f4a904c027fe09ae71884ba26a69d589ff082e0 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 22 Jul 2025 16:49:35 +0530 Subject: [PATCH 14/44] Secrets manager client add and remove commands --- .../src/keepercli/commands/secrets_manager.py | 437 +++++++++++++++++- .../src/keepercli/register_commands.py | 1 + 2 files changed, 427 insertions(+), 11 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index e8f88c17..c7266d3a 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -1,34 +1,44 @@ import argparse +import datetime from enum import Enum +import hmac +import os +import time from typing import Optional +from urllib import parse +from keepersdk import crypto, utils +from keepersdk.proto.APIRequest_pb2 import AddAppClientRequest, Device, RemoveAppClientsRequest +from keepersdk.proto.enterprise_pb2 import GENERAL from keepersdk.vault import ksm_management, vault_online from . import base -from .share_management import ShareAction, ShareRecordCommand, ShareFolderCommand -from .. import api +from .share_management import ShareAction, ShareFolderCommand, ShareRecordCommand +from .. import api, constants, prompt_utils from ..helpers import ksm_utils, report_utils, share_utils from ..params import KeeperParams logger = api.get_logger() +CLIENT_ADD_URL = 'vault/app_client_add' +CLIENT_REMOVE_URL = 'vault/app_client_remove' class SecretsManagerCommand(Enum): LIST = "list" GET = "get" + ADD = 'add' CREATE = "create" REMOVE = "remove" SHARE = "share" UNSHARE = "unshare" - class SecretsManagerAppCommand(base.ArgparseCommand): def __init__(self): self.parser = argparse.ArgumentParser( prog='secrets-manager app', - description='Keeper Secrets Manager (KSM) Commands', + description='Keeper Secrets Manager (KSM) App Commands', ) SecretsManagerAppCommand.add_arguments_to_parser(self.parser) super().__init__(self.parser) @@ -37,12 +47,12 @@ def __init__(self): def add_arguments_to_parser(parser: argparse.ArgumentParser): parser.add_argument( - '--command', type=str, action='store', required=True, dest='command', + '--command', type=str, action='store', dest='command', choices=[cmd.value for cmd in SecretsManagerCommand], - help='One of: "list", "get", "create", "remove", "share" or "unshare"' + help = f"One of: {', '.join(cmd.value for cmd in SecretsManagerCommand)}" ) parser.add_argument( - '--name', '-n', type=str, dest='name', action='store', required=False, help='Application Name or UID' + '--app', '-n', type=str, dest='app', action='store', help='Application Name or UID' ) parser.add_argument( '-f', '--force', dest='force', action='store_true', help='Force add or remove app' @@ -60,13 +70,13 @@ def execute(self, context: KeeperParams, **kwargs) -> None: vault = context.vault command = kwargs.get('command') - uid_or_name = kwargs.get('name') + uid_or_name = kwargs.get('app') force = kwargs.get('force') email = kwargs.get('email') is_admin = kwargs.get('admin', False) if not command: - raise ValueError("Command is required. Available commands: list, get, create, remove, share, unshare") + return self.get_parser().print_help() if command != SecretsManagerCommand.LIST.value and not uid_or_name: raise ValueError("Application name or UID is required. Use --name='example' to set it.") @@ -97,6 +107,7 @@ def unshare_app(): SecretsManagerCommand.LIST.value: list_app, SecretsManagerCommand.GET.value: get_app, SecretsManagerCommand.CREATE.value: create_app, + SecretsManagerCommand.ADD.value: create_app, SecretsManagerCommand.REMOVE.value: remove_app, SecretsManagerCommand.SHARE.value: share_app, SecretsManagerCommand.UNSHARE.value: unshare_app @@ -301,8 +312,9 @@ def _user_needs_update(vault: vault_online.VaultOnline, user: str, share_uids: l else: share_user_permissions = [] - # Check if user already has permissions - if not any(up.get('username') == user for up in share_user_permissions if isinstance(up, dict)): + # Check if user already has permissions using hashmap for O(1) lookup + user_permissions_set = {up.get('username') for up in share_user_permissions if isinstance(up, dict)} + if user not in user_permissions_set: return True return False @@ -371,3 +383,406 @@ def _create_record_share_requests(context: KeeperParams, shared_recs: list, ) requests.append(request) return requests + + +class SecretsManagerClientCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='secrets-manager-client', + description='Keeper Secrets Manager (KSM) Client Commands', + ) + SecretsManagerClientCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + + parser.add_argument( + '--command', type=str, action='store', dest='command', + choices=[SecretsManagerCommand.ADD.value, SecretsManagerCommand.REMOVE.value], + help = f"One of: {SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}" + ) + parser.add_argument( + '--app', '-a', type=str, action='store', help='Application Name or UID' + ) + parser.add_argument( + '--name', '-n', type=str, dest='name', action='store', required=False, help='client name' + ) + parser.add_argument( + '--client', '-i', type=str, dest='client_names_or_ids', action='append', required=False, + help='Client Name or ID. Use * or all to remove all clients' + ) + parser.add_argument( + '--unlock-ip', '-l', dest='unlockIp', action='store_true', help='Unlock IP Address.' + ) + parser.add_argument( + '--return-tokens', dest='returnTokens', action='store_true', help='Return Tokens' + ) + parser.add_argument( + '--secret', '-s', type=str, action='append', required=False, help='Record UID' + ) + parser.add_argument( + '--count', '-c', type=int, dest='count', action='store', + help='Number of tokens to return. Default: 1', default=1 + ) + parser.add_argument( + '--first-access-expires-in-min', '-x', type=int, dest='firstAccessExpiresIn', action='store', + help='Time for the first request to expire in minutes from the time when this command is executed. ' + 'Maximum 1440 minutes (24 hrs). Default: 60', default=60 + ) + parser.add_argument( + '-f', '--force', dest='force', action='store_true', help='Force add or remove app' + ) + parser.add_argument( + '--access-expire-in-min', '-p', type=int, dest='accessExpireInMin', action='store', + help='Time interval that this client can access the KSM application. After this time, access is denied. ' + 'Time is entered in minutes starting from the time when command is executed. Default: Not expiration' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + vault = context.vault + command = kwargs.get('command') + uid_or_name = kwargs.get('app') + + if not command: + return self.get_parser().print_help() + + if not uid_or_name: + raise ValueError('Application UID or name is required. Use --app="uid_or_name".') + + ksm_app = next((r for r in vault.vault_data.records() if r.record_uid == uid_or_name or r.title == uid_or_name), None) + if not ksm_app: + raise ValueError(f'No application found with UID/Name: {uid_or_name}') + uid = ksm_app.record_uid + + if command == SecretsManagerCommand.ADD.value: + count = kwargs.get('count', 1) + unlock_ip = kwargs.get('unlockIp', False) + + client_name = kwargs.get('name') + + first_access_expire_in = kwargs.get('firstAccessExpiresIn', 60) + access_expire_in_min = kwargs.get('accessExpireInMin') + + is_return_tokens = kwargs.get('returnTokens', False) + + tokens_and_device = SecretsManagerClientCommand.add_client( + vault=vault, uid=uid, count=count, client_name=client_name, + unlock_ip=unlock_ip, first_access_expire_duration=first_access_expire_in, + access_expire_in_min=access_expire_in_min, server=context.server + ) + + tokens_only = [d['oneTimeToken'] for d in tokens_and_device] + + return ', '.join(tokens_only) if is_return_tokens else None + + elif command == SecretsManagerCommand.REMOVE.value: + client_names_or_ids = kwargs.get('client_names_or_ids') + if not client_names_or_ids: + raise ValueError('Client name or id is required. Example: --client="new client"') + + force = kwargs.get('force', False) + + if len(client_names_or_ids) == 1 and client_names_or_ids[0] in ['*', 'all']: + SecretsManagerClientCommand.remove_all_clients(vault=vault, uid=uid, force=force) + else: + SecretsManagerClientCommand.remove_client(vault=vault, uid=uid, client_names_and_ids=client_names_or_ids) + + return + + @staticmethod + def add_client( + vault: vault_online.VaultOnline, + uid: str, + count: int, + client_name: str, + unlock_ip: bool, + first_access_expire_duration: int, + access_expire_in_min: Optional[int], + server: str): + + current_time_ms = int(time.time( ) * 1000) + + first_access_expire_duration_ms = current_time_ms + first_access_expire_duration * 60 * 1000 + access_expire_in_ms = None + if access_expire_in_min: + access_expire_in_ms = access_expire_in_min * 60 * 1000 + + master_key = vault.vault_data.get_record_key(record_uid=uid) + + tokens = [] + output_lines = [] + + for i in range(count): + token_data = SecretsManagerClientCommand._generate_single_client( + vault=vault, + uid=uid, + client_name=client_name, + count=count, + index=i, + unlock_ip=unlock_ip, + first_access_expire_duration_ms=first_access_expire_duration_ms, + access_expire_in_ms=access_expire_in_ms, + master_key=master_key, + server=server + ) + + tokens.append(token_data['token_info']) + output_lines.append(token_data['output_string']) + + one_time_access_token = ''.join(output_lines) + SecretsManagerClientCommand._log_success_message(one_time_access_token) + + if not unlock_ip: + SecretsManagerClientCommand._log_ip_lock_warning() + + return tokens + + @staticmethod + def _generate_single_client( + vault: vault_online.VaultOnline, + uid: str, + client_name: str, + count: int, + index: int, + unlock_ip: bool, + first_access_expire_duration_ms: int, + access_expire_in_ms: Optional[int], + master_key: bytes, + server: str) -> dict: + """Generate a single client device and return token info and output string.""" + + # Generate secret and client ID + secret_bytes = os.urandom(32) + client_id = SecretsManagerClientCommand._generate_client_id(secret_bytes) + + encrypted_master_key = crypto.encrypt_aes_v2(master_key, secret_bytes) + + # Create and send request + device = SecretsManagerClientCommand._create_client_request( + vault=vault, + uid=uid, + encrypted_master_key=encrypted_master_key, + unlock_ip=unlock_ip, + first_access_expire_duration_ms=first_access_expire_duration_ms, + access_expire_in_ms=access_expire_in_ms, + client_id=client_id, + client_name=client_name, + count=count, + index=index + ) + + # Generate token with server prefix + token_with_prefix = SecretsManagerClientCommand._generate_token_with_prefix( + secret_bytes=secret_bytes, + server=server + ) + + output_string = SecretsManagerClientCommand._create_output_string( + token_with_prefix=token_with_prefix, + client_name=client_name, + unlock_ip=unlock_ip, + first_access_expire_duration_ms=first_access_expire_duration_ms, + access_expire_in_ms=access_expire_in_ms + ) + + return { + 'token_info': { + 'oneTimeToken': token_with_prefix, + 'deviceToken': utils.base64_url_encode(device.encryptedDeviceToken) + }, + 'output_string': output_string + } + + @staticmethod + def _generate_client_id(secret_bytes: bytes) -> bytes: + """Generate client ID using HMAC.""" + counter_bytes = b'KEEPER_SECRETS_MANAGER_CLIENT_ID' + digest = 'sha512' + + try: + return hmac.new(secret_bytes, counter_bytes, digest).digest() + except Exception as e: + logger.error(e) + raise + + @staticmethod + def _create_client_request( + vault: vault_online.VaultOnline, + uid: str, + encrypted_master_key: bytes, + unlock_ip: bool, + first_access_expire_duration_ms: int, + access_expire_in_ms: Optional[int], + client_id: bytes, + client_name: str, + count: int, + index: int) -> Device: + """Create and send client request to server.""" + + request = AddAppClientRequest() + request.appRecordUid = utils.base64_url_decode(uid) + request.encryptedAppKey = encrypted_master_key + request.lockIp = not unlock_ip + request.firstAccessExpireOn = first_access_expire_duration_ms + request.appClientType = GENERAL + request.clientId = client_id + + if access_expire_in_ms: + request.accessExpireOn = access_expire_in_ms + + if client_name: + request.id = client_name if count == 1 else f"{client_name} {index + 1}" + + device = vault.keeper_auth.execute_auth_rest( + rest_endpoint=CLIENT_ADD_URL, + request=request, + response_type=Device + ) + + if not device or not device.encryptedDeviceToken: + raise ValueError("Failed to create client device - no device token received") + + return device + + @staticmethod + def _generate_token_with_prefix(secret_bytes: bytes, server: str) -> str: + """Generate token with server prefix.""" + token = utils.base64_url_encode(secret_bytes) + + # Get server abbreviation + abbrev = constants.get_abbrev_by_host(server) + + if abbrev: + return f'{abbrev}:{token}' + else: + tmp_server = server if server.startswith(('http://', 'https://')) else f"https://{server}" + + return f'{parse.urlparse(tmp_server).netloc.lower()}:{token}' + + @staticmethod + def _create_output_string( + token_with_prefix: str, + client_name: str, + unlock_ip: bool, + first_access_expire_duration_ms: int, + access_expire_in_ms: Optional[int]) -> str: + """Create formatted output string for logging.""" + + output_lines = [f'\nOne-Time Access Token: {token_with_prefix}'] + + if client_name: + output_lines.append(f'Name: {client_name}') + + ip_lock = 'Disabled' if unlock_ip else 'Enabled' + output_lines.append(f'IP Lock: {ip_lock}') + + try: + exp_date_str = datetime.datetime.fromtimestamp( + first_access_expire_duration_ms / 1000 + ).strftime('%Y-%m-%d %H:%M:%S') + except (OSError, ValueError) as e: + exp_date_str = 'Invalid timestamp' + output_lines.append(f'Token Expires On: {exp_date_str}') + + if access_expire_in_ms: + app_expire_on_str = datetime.datetime.fromtimestamp( + access_expire_in_ms / 1000 + ).strftime('%Y-%m-%d %H:%M:%S') + else: + app_expire_on_str = "Never" + + output_lines.append(f'App Access Expires on: {app_expire_on_str}') + + return '\n'.join(output_lines) + + @staticmethod + def _log_success_message(output_string: str) -> None: + """Log success message with generated client information.""" + logger.info(f'\nSuccessfully generated Client Device\n' + f'====================================\n' + f'{output_string}') + + @staticmethod + def _log_ip_lock_warning() -> None: + """Log warning about IP lock configuration.""" + logger.info("Warning: Configuration is now locked to your current IP. To keep in unlock you can add flag `--unlock-ip` " + "or use the One-time token to generate configuration on the host that has the IP that needs to be locked.") + logger.warning('') + + @staticmethod + def remove_all_clients(vault: vault_online.VaultOnline, uid: str, force: bool): + + app_info = ksm_management.get_app_info(vault=vault, app_uid=uid) + + clients_count = len(app_info[0].clients) + + if clients_count == 0: + logger.warning('No client devices registered for this Application\n') + return + + if not force: + logger.info(f"This app has {clients_count} client(s) connections.") + uc = prompt_utils.user_choice('Are you sure you want to delete all clients from this application?', 'yn', default='n') + if uc.lower() != 'y': + return + + client_ids_to_remove = [utils.base64_url_encode(c.clientId) for ai in app_info + for c in ai.clients if c.appClientType == GENERAL] + + if len(client_ids_to_remove) > 0: + SecretsManagerClientCommand.remove_client(vault=vault, uid=uid, client_names_and_ids=client_ids_to_remove) + + @staticmethod + def remove_client(vault: vault_online.VaultOnline, uid: str, client_names_and_ids: list[str]): + + def convert_ids_and_hashes_to_hashes(client_names_and_ids, uid): + exact_matches = set() + partial_matches = set() + + for name in client_names_and_ids: + if len(name) >= ksm_management.CLIENT_SHORT_ID_LENGTH: + partial_matches.add(name) + else: + exact_matches.add(name) + + client_id_hashes_bytes = [] + app_infos = ksm_management.get_app_info(vault=vault, app_uid=uid) + app_info = app_infos[0] + + for client in app_info.clients: + if client.id in exact_matches: + client_id_hashes_bytes.append(client.clientId) + continue + + if partial_matches: + client_id = utils.base64_url_encode(client.clientId) + for partial_name in partial_matches: + if client_id.startswith(partial_name): + client_id_hashes_bytes.append(client.clientId) + break + + return client_id_hashes_bytes + + client_hashes = convert_ids_and_hashes_to_hashes(client_names_and_ids=client_names_and_ids, uid=uid) + + found_clients_count = len(client_hashes) + if found_clients_count == 0: + logger.warning('No Client Devices found with given name or ID\n') + return + else: + uc = prompt_utils.user_choice(f'Are you sure you want to delete {found_clients_count} matching client(s) from this application?', + 'yn', default='n') + if uc.lower() != 'y': + return + + request = RemoveAppClientsRequest() + + request.appRecordUid = utils.base64_url_decode(uid) + request.clients.extend(client_hashes) + vault.keeper_auth.execute_auth_rest(rest_endpoint=CLIENT_REMOVE_URL, request=request) + logger.info('\nClient removal was successful\n') diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 033f08c7..8d92185d 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -52,6 +52,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('load-record-types', record_type.LoadRecordTypesCommand(), base.CommandScope.Vault) commands.register_command('download-record-types', record_type.DownloadRecordTypesCommand(), base.CommandScope.Vault) commands.register_command('secrets-manager-app', secrets_manager.SecretsManagerAppCommand(), base.CommandScope.Vault) + commands.register_command('secrets-manager-client', secrets_manager.SecretsManagerClientCommand(), base.CommandScope.Vault) commands.register_command('share-record', share_management.ShareRecordCommand(), base.CommandScope.Vault, 'sr') commands.register_command('share-folder', share_management.ShareFolderCommand(), base.CommandScope.Vault, 'sf') From b582d8220e3a09fd085f325c7e0ec3fd5ef4f7bb Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 25 Jul 2025 18:21:21 +0530 Subject: [PATCH 15/44] Added secrets-manager-share add and remove commands --- .../src/keepercli/commands/secrets_manager.py | 215 +++++++++++++++++- .../src/keepercli/register_commands.py | 1 + 2 files changed, 208 insertions(+), 8 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index c7266d3a..8500094f 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -8,7 +8,7 @@ from urllib import parse from keepersdk import crypto, utils -from keepersdk.proto.APIRequest_pb2 import AddAppClientRequest, Device, RemoveAppClientsRequest +from keepersdk.proto.APIRequest_pb2 import AddAppClientRequest, Device, RemoveAppClientsRequest, AppShareAdd, ApplicationShareType, AddAppSharesRequest, RemoveAppSharesRequest from keepersdk.proto.enterprise_pb2 import GENERAL from keepersdk.vault import ksm_management, vault_online @@ -22,6 +22,10 @@ logger = api.get_logger() CLIENT_ADD_URL = 'vault/app_client_add' CLIENT_REMOVE_URL = 'vault/app_client_remove' +SHARE_ADD_URL = 'vault/app_share_add' +SHARE_REMOVE_URL = 'vault/app_share_remove' +RECORD = 'Record' +SHARED_FOLDER = 'Shared Folder' class SecretsManagerCommand(Enum): @@ -233,9 +237,9 @@ def _separate_shared_items(vault: vault_online.VaultOnline, shared_secrets): for share in shared_secrets: uid_str = utils.base64_url_encode(share.secretUid) share_type = ApplicationShareType.Name(share.shareType) - if share_type == 'SHARE_TYPE_RECORD': + if share_type == ApplicationShareType.SHARE_TYPE_RECORD: shared_recs.append(uid_str) - elif share_type == 'SHARE_TYPE_FOLDER': + elif share_type == ApplicationShareType.SHARE_TYPE_FOLDER: shared_folders.append(uid_str) if shared_recs: @@ -312,7 +316,6 @@ def _user_needs_update(vault: vault_online.VaultOnline, user: str, share_uids: l else: share_user_permissions = [] - # Check if user already has permissions using hashmap for O(1) lookup user_permissions_set = {up.get('username') for up in share_user_permissions if isinstance(up, dict)} if user not in user_permissions_set: return True @@ -490,9 +493,12 @@ def execute(self, context: KeeperParams, **kwargs) -> None: if len(client_names_or_ids) == 1 and client_names_or_ids[0] in ['*', 'all']: SecretsManagerClientCommand.remove_all_clients(vault=vault, uid=uid, force=force) else: - SecretsManagerClientCommand.remove_client(vault=vault, uid=uid, client_names_and_ids=client_names_or_ids) + SecretsManagerClientCommand.remove_client(vault=vault, uid=uid, client_names_and_ids=client_names_or_ids, force=force) return + else: + raise base.CommandError(f"Unknown command '{command}'. Available commands: {SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}") + @staticmethod def add_client( @@ -735,10 +741,10 @@ def remove_all_clients(vault: vault_online.VaultOnline, uid: str, force: bool): for c in ai.clients if c.appClientType == GENERAL] if len(client_ids_to_remove) > 0: - SecretsManagerClientCommand.remove_client(vault=vault, uid=uid, client_names_and_ids=client_ids_to_remove) + SecretsManagerClientCommand.remove_client(vault=vault, uid=uid, client_names_and_ids=client_ids_to_remove, force=force) @staticmethod - def remove_client(vault: vault_online.VaultOnline, uid: str, client_names_and_ids: list[str]): + def remove_client(vault: vault_online.VaultOnline, uid: str, client_names_and_ids: list[str], force=False): def convert_ids_and_hashes_to_hashes(client_names_and_ids, uid): exact_matches = set() @@ -774,7 +780,7 @@ def convert_ids_and_hashes_to_hashes(client_names_and_ids, uid): if found_clients_count == 0: logger.warning('No Client Devices found with given name or ID\n') return - else: + if not force: uc = prompt_utils.user_choice(f'Are you sure you want to delete {found_clients_count} matching client(s) from this application?', 'yn', default='n') if uc.lower() != 'y': @@ -786,3 +792,196 @@ def convert_ids_and_hashes_to_hashes(client_names_and_ids, uid): request.clients.extend(client_hashes) vault.keeper_auth.execute_auth_rest(rest_endpoint=CLIENT_REMOVE_URL, request=request) logger.info('\nClient removal was successful\n') + + +class SecretsManagerShareCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='secrets-manager-share', + description='Keeper Secrets Manager (KSM) Share Commands', + ) + SecretsManagerShareCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '--command', type=str, action='store', dest='command', + choices=[SecretsManagerCommand.ADD.value, SecretsManagerCommand.REMOVE.value], + help=f"One of: {SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}" + ) + parser.add_argument( + '--editable', '-e', action='store_true', required=False, + help='Is this share going to be editable or not' + ) + parser.add_argument( + '--app', '-a', type=str, action='store', help='Application Name or UID' + ) + parser.add_argument( + '--secret', '-s', type=str, required=False, + help='Record UID(s) - space separated (e.g., "uid1 uid2 uid3")' + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + vault = context.vault + command = kwargs.get('command') + app_uid_or_name = kwargs.get('app') + secret_uids_str = kwargs.get('secret') + secret_uids = [] + if secret_uids_str: + secret_uids = [uid.strip() for uid in secret_uids_str.split() if uid.strip()] + + if not command: + return self.get_parser().print_help() + + if not app_uid_or_name: + raise ValueError('Application UID or name is required. Use --app="uid_or_name".') + + ksm_app = self._find_ksm_application(vault, app_uid_or_name) + if not ksm_app: + raise ValueError(f'No application found with UID/Name: {app_uid_or_name}') + + app_uid = ksm_app.record_uid + + if command == SecretsManagerCommand.ADD.value: + is_editable = kwargs.get('editable', False) + self._handle_add_share(context, app_uid, secret_uids, is_editable) + elif command == SecretsManagerCommand.REMOVE.value: + SecretsManagerShareCommand.remove_share(vault=vault, app_uid=app_uid, secret_uids=secret_uids) + else: + raise base.CommandError(f"Unknown command '{command}'. Available commands: {SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}") + + def _find_ksm_application(self, vault: vault_online.VaultOnline, app_uid_or_name: str): + return next( + (r for r in vault.vault_data.records() + if r.record_uid == app_uid_or_name or r.title == app_uid_or_name), + None + ) + + def _handle_add_share(self, context: KeeperParams, app_uid: str, secret_uids: list[str], is_editable: bool) -> None: + if not context.vault: + raise ValueError("Vault is not initialized.") + + master_key = context.vault.vault_data.get_record_key(record_uid=app_uid) + if not master_key: + raise ValueError(f"Could not retrieve master key for application {app_uid}") + + success = SecretsManagerShareCommand.share_secret( + vault=context.vault, + app_uid=app_uid, + secret_uids=secret_uids, + master_key=master_key, + is_editable=is_editable + ) + + if success: + context.vault.sync_down() + SecretsManagerAppCommand.update_shares_user_permissions(context=context, uid=app_uid, removed=False) + + @staticmethod + def share_secret(vault: vault_online.VaultOnline, app_uid: str, master_key: bytes, + secret_uids: list[str], is_editable: bool = False) -> bool: + if not secret_uids: + logger.warning("No secret UIDs provided for sharing.") + return False + + app_shares = [] + added_secret_info = [] + + for secret_uid in secret_uids: + share_info = SecretsManagerShareCommand._process_secret( + vault, secret_uid, master_key, is_editable + ) + + if share_info: + app_shares.append(share_info['app_share']) + added_secret_info.append(share_info['secret_info']) + + if not added_secret_info: + logger.warning("No valid secrets found to share.") + return False + + return SecretsManagerShareCommand._send_share_request( + vault, app_uid, app_shares, added_secret_info, is_editable + ) + + @staticmethod + def _process_secret(vault: vault_online.VaultOnline, secret_uid: str, + master_key: bytes, is_editable: bool) -> Optional[dict]: + is_record = secret_uid in vault.vault_data._records + is_shared_folder = secret_uid in vault.vault_data._shared_folders + + if is_record: + share_key_decrypted = vault.vault_data.get_record_key(record_uid=secret_uid) + share_type = ApplicationShareType.SHARE_TYPE_RECORD + secret_type_name = RECORD + elif is_shared_folder: + share_key_decrypted = vault.vault_data.get_shared_folder_key(shared_folder_uid=secret_uid) + share_type = ApplicationShareType.SHARE_TYPE_FOLDER + secret_type_name = SHARED_FOLDER + else: + logger.error( + f"UID='{secret_uid}' is not a Record nor Shared Folder. " + "Only individual records or Shared Folders can be added to the application. " + "Make sure your local cache is up to date by running 'sync-down' command and trying again." + ) + return None + + if not share_key_decrypted: + logger.error(f"Could not retrieve key for secret {secret_uid}") + return None + + app_share = AppShareAdd() + app_share.secretUid = utils.base64_url_decode(secret_uid) + app_share.shareType = share_type + app_share.encryptedSecretKey = crypto.encrypt_aes_v2(share_key_decrypted, master_key) + app_share.editable = is_editable + + return { + 'app_share': app_share, + 'secret_info': (secret_uid, secret_type_name) + } + + @staticmethod + def _send_share_request(vault: vault_online.VaultOnline, app_uid: str, + app_shares: list, added_secret_info: list, is_editable: bool) -> bool: + """Send the share request to the server.""" + request = AddAppSharesRequest() + request.appRecordUid = utils.base64_url_decode(app_uid) + request.shares.extend(app_shares) + + try: + vault.keeper_auth.execute_auth_rest(rest_endpoint=SHARE_ADD_URL, request=request) + + logger.info(f'\nSuccessfully added secrets to app uid={app_uid}, editable={is_editable}:') + for secret_uid, secret_type in added_secret_info: + logger.info(f'{secret_uid} \t{secret_type}') + return True + + except base.errors.KeeperApiError as kae: + if kae.message == 'Duplicate share, already added': + logger.error( + "One of the secret UIDs is already shared to this application. " + "Please remove already shared UIDs from your command and try again." + ) + else: + logger.error(f"Failed to share secrets: {kae}") + return False + + @staticmethod + def remove_share(vault: vault_online.VaultOnline, app_uid: str, secret_uids: list[str]) -> None: + """Remove shares from a KSM application.""" + if not secret_uids: + logger.warning("No secret UIDs provided for removal.") + return + + request = RemoveAppSharesRequest() + request.appRecordUid = utils.base64_url_decode(app_uid) + request.shares.extend(utils.base64_url_decode(uid) for uid in secret_uids) + + vault.keeper_auth.execute_auth_rest(rest_endpoint=SHARE_REMOVE_URL, request=request) + logger.info("Shared secrets were successfully removed from the application\n") \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 8d92185d..a4211ed2 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -53,6 +53,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('download-record-types', record_type.DownloadRecordTypesCommand(), base.CommandScope.Vault) commands.register_command('secrets-manager-app', secrets_manager.SecretsManagerAppCommand(), base.CommandScope.Vault) commands.register_command('secrets-manager-client', secrets_manager.SecretsManagerClientCommand(), base.CommandScope.Vault) + commands.register_command('secrets-manager-share', secrets_manager.SecretsManagerShareCommand(), base.CommandScope.Vault) commands.register_command('share-record', share_management.ShareRecordCommand(), base.CommandScope.Vault, 'sr') commands.register_command('share-folder', share_management.ShareFolderCommand(), base.CommandScope.Vault, 'sf') From 7956b879f7c5c6c7bf9ac6669b4670370bc78223 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 1 Aug 2025 18:34:25 +0530 Subject: [PATCH 16/44] Added get command and self-destruct feature --- .../src/keepercli/commands/record_edit.py | 645 +++++++++++++++++- .../src/keepercli/helpers/folder_utils.py | 26 + .../src/keepercli/helpers/record_utils.py | 111 ++- .../src/keepercli/register_commands.py | 1 + .../src/keepersdk/vault/vault_record.py | 16 + 5 files changed, 783 insertions(+), 16 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index e23d53e3..24192cad 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -12,14 +12,18 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 from keepersdk.vault import (record_types, typed_field_utils, vault_record, attachment, record_facades, - record_management, vault_online, vault_data, vault_types, vault_utils) + record_management, vault_online, vault_data, vault_types, vault_utils, vault_extensions) from keepersdk import crypto, generator from . import base from .. import prompt_utils, api, constants -from ..helpers import folder_utils, record_utils +from ..helpers import folder_utils, record_utils, share_utils, timeout_utils from ..params import KeeperParams + +logger = api.get_logger() + + @dataclasses.dataclass(frozen=True) class ParsedFieldValue: section: str @@ -126,15 +130,13 @@ class ParsedFieldValue: class RecordEditMixin(typed_field_utils.TypedFieldMixin): def __init__(self) -> None: self.warnings: List[str] = [] - self.logger = api.get_logger() def on_warning(self, message: str) -> None: if message: self.warnings.append(message) def on_info(self, message): - if self.logger: - self.logger.info(message) + logger.info(message) @staticmethod def parse_field(field: str) -> ParsedFieldValue: @@ -619,6 +621,10 @@ class RecordAddCommand(base.ArgparseCommand, RecordEditMixin): help='folder name or UID to store record') parser.add_argument('fields', nargs='*', type=str, help='load record type data from strings with dot notation') + parser.add_argument('--self-destruct', dest='self_destruct', action='store', + metavar='[(m)inutes|(h)ours|(d)ays]', + help='Time period record share URL is valid. The record will be deleted in your vault in 5 minutes since open') + def __init__(self): super().__init__(RecordAddCommand.parser) @@ -682,7 +688,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: ignore_warnings = kwargs.get('force') is True if len(self.warnings) > 0: for warning in self.warnings: - api.get_logger().warning(warning) + logger.warning(warning) if not ignore_warnings: return self.warnings.clear() @@ -691,13 +697,26 @@ def execute(self, context: KeeperParams, **kwargs) -> None: self.upload_attachments(context.vault, record, add_attachments, not ignore_warnings) if len(self.warnings) > 0: for warning in self.warnings: - api.get_logger().warning(warning) + logger.warning(warning) if not ignore_warnings: return + self_destruct = kwargs.get('self_destruct') + record_management.add_record_to_folder(context.vault, record, folder.folder_uid) context.environment_variables[constants.LAST_RECORD_UID] = record.record_uid - return record.record_uid + if not self_destruct: + return record.record_uid + else: + expiration_period = None + expiration_period = timeout_utils.parse_timeout(self_destruct) + if expiration_period.total_seconds() > 182 * 24 * 60 * 60: + raise base.CommandError('URL expiration period cannot be greater than 6 months.') + url = record_utils.process_external_share(context=context, expiration_period=expiration_period, record=record) + expiration_date = datetime.datetime.now() + expiration_period + formatted_date = expiration_date.strftime('%d/%m/%Y %H:%M:%S') + message = f'Record self-destructs on {formatted_date} or after being viewed once. Once the link is opened the recipient will have 5 minutes to view the record.\n{url}' + return message class RecordUpdateCommand(base.ArgparseCommand, RecordEditMixin): parser = argparse.ArgumentParser(prog='record-update', description='Update a record') @@ -778,7 +797,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: ignore_warnings = kwargs.get('force') is True if len(self.warnings) > 0: for warning in self.warnings: - api.get_logger().warning(warning) + logger.warning(warning) if not ignore_warnings: return self.warnings.clear() @@ -788,7 +807,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: self.delete_attachments(context.vault.vault_data, record, names) if len(self.warnings) > 0: for warning in self.warnings: - api.get_logger().warning(warning) + logger.warning(warning) if not ignore_warnings: return self.warnings.clear() @@ -797,7 +816,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: self.upload_attachments(context.vault, record, add_attachments, not ignore_warnings) if len(self.warnings) > 0: for warning in self.warnings: - api.get_logger().warning(warning) + logger.warning(warning) if not ignore_warnings: return @@ -863,7 +882,7 @@ def execute(self, context, **kwargs): typed_field.value = [x for x in typed_field.value if x not in deleted_files] if len(deleted_files) == 0: - api.get_logger().info('Attachment(s) not found') + logger.info('Attachment(s) not found') return record_management.update_record(context.vault, record) @@ -1026,3 +1045,605 @@ def execute(self, context: KeeperParams, **kwargs) -> None: confirm=confirm_fn ) + +class RecordGetCommand(base.ArgparseCommand): + """Command to get details of Records, Folders, Teams by UID or title.""" + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='get', + description='Get the details of a Record/Folder/Team by UID or title' + ) + RecordGetCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '--unmask', dest='unmask', action='store_true', + help='display hidden field content' + ) + parser.add_argument( + '--legacy', dest='legacy', action='store_true', + help='json output: display typed records as legacy' + ) + parser.add_argument( + '--format', dest='format', action='store', + choices=['detail', 'json', 'password', 'fields'], + default='detail', + help='output format as detail, json, password, fields' + ) + parser.add_argument( + 'uid', type=str, action='store', + help='UID or title to search for' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the get command based on the provided parameters.""" + self._validate_context(context) + + uid = kwargs.get('uid') + output_format = kwargs.get('format', 'detail') + unmask = kwargs.get('unmask', False) + + if not uid: + raise base.CommandError('UID parameter is required') + + target_object = self._find_target_object(context.vault, uid) + if not target_object: + raise base.CommandError('The given UID is not a valid Keeper Object') + + self._display_object(context.vault, target_object, output_format, unmask) + + def _validate_context(self, context: KeeperParams): + """Validate that the vault is properly initialized.""" + if not context.vault: + raise ValueError("Vault is not initialized.") + + def _find_target_object(self, vault: vault_data.VaultData, uid_or_title: str): + """Find a Keeper object (record, folder, shared folder, or team) by UID or title.""" + + shared_folder = self._find_shared_folder(vault, uid_or_title) + if shared_folder: + return ('shared_folder', shared_folder) + + folder = self._find_folder(vault, uid_or_title) + if folder: + return ('folder', folder) + + team = self._find_team(vault, uid_or_title) + if team: + return ('team', team) + + record = self._find_record(vault, uid_or_title) + if record: + return ('record', record) + + return None + + def _find_record(self, vault: vault_data.VaultData, uid_or_title: str): + """Find a record by UID or title.""" + return next( + (r for r in vault.vault_data.records() + if r.record_uid == uid_or_title or r.title == uid_or_title), + None + ) + + def _find_shared_folder(self, vault: vault_data.VaultData, uid_or_title: str): + """Find a shared folder by UID or name.""" + return next( + (f for f in vault.vault_data.shared_folders() + if f.shared_folder_uid == uid_or_title or f.name == uid_or_title), + None + ) + + def _find_folder(self, vault: vault_data.VaultData, uid_or_title: str): + """Find a folder by UID or name.""" + return next( + (f for f in vault.vault_data.folders() + if f.folder_uid == uid_or_title or f.name == uid_or_title), + None + ) + + def _find_team(self, vault: vault_data.VaultData, uid_or_title: str): + """Find a team by UID or name.""" + return next( + (t for t in vault.vault_data.teams() + if t.team_uid == uid_or_title or t.name == uid_or_title), + None + ) + + def _display_object(self, vault: vault_data.VaultData, target_object, output_format: str, unmask: bool): + """Display the target object in the specified format.""" + object_type, object_data = target_object + + if object_type == 'record': + self._display_record(vault, object_data, output_format, unmask) + elif object_type == 'shared_folder': + self._display_shared_folder(vault, object_data, output_format) + elif object_type == 'folder': + self._display_folder(vault, object_data, output_format) + elif object_type == 'team': + self._display_team(vault, object_data, output_format) + + def _display_record(self, vault: vault_data.VaultData, record, output_format: str, unmask: bool): + """Display a record in the specified format.""" + record_uid = record.record_uid + dispatch = { + 'json': lambda: self._display_record_json(vault, record_uid), + 'password': lambda: self._display_record_password(vault, record_uid), + 'fields': lambda: self._display_record_fields(vault, record_uid, unmask) + } + + display_func = dispatch.get(output_format, lambda: self._display_record_detail(vault, record_uid, unmask)) + display_func() + + def _display_shared_folder(self, vault: vault_data.VaultData, shared_folder, output_format: str): + """Display a shared folder in the specified format.""" + if output_format == 'json': + self._display_shared_folder_json(vault, shared_folder.shared_folder_uid) + else: # detail format + self._display_shared_folder_detail(vault, shared_folder.shared_folder_uid) + + def _display_folder(self, vault: vault_data.VaultData, folder, output_format: str): + """Display a folder in the specified format.""" + if output_format == 'json': + self._display_folder_json(vault, folder.folder_uid) + else: # detail format + self._display_folder_detail(vault, folder.folder_uid) + + def _display_team(self, vault: vault_data.VaultData, team, output_format: str): + """Display a team in the specified format.""" + if output_format == 'json': + self._display_team_json(vault, team.team_uid) + else: # detail format + self._display_team_detail(vault, team.team_uid) + + def _display_record_json(self, vault: vault_data.VaultData, uid: str): + """Display record information in JSON format.""" + record = vault.vault_data.get_record(record_uid=uid) + record_data = vault.vault_data.load_record(record_uid=uid) + + output = self._build_record_json_output(record, record_data, uid) + + self._add_share_info_to_json(vault, uid, output) + + logger.info(json.dumps(output, indent=2)) + + def _build_record_json_output(self, record, record_data, uid: str): + """Build the JSON output structure for a record.""" + output = { + 'Record UID:': uid, + 'Type': record.record_type, + 'Title:': record.title, + } + + if isinstance(record_data, vault_record.PasswordRecord): + self._add_password_record_json_fields(record_data, output) + elif isinstance(record_data, vault_record.TypedRecord): + self._add_typed_record_json_fields(record_data, output) + elif isinstance(record_data, vault_record.FileRecord): + self._add_file_record_json_fields(record_data, output) + else: + raise ValueError('Record data could not be displayed. Record is of unsupported type for this command(eg Application record)') + + output['Last Modified:'] = record_data.client_time_modified + output['Version:'] = record.version + output['Revision'] = record.revision + + return output + + def _add_password_record_json_fields(self, record_data: vault_record.PasswordRecord, output: dict): + """Add password record specific fields to JSON output.""" + output['Notes:'] = record_data.notes + output['$login:'] = record_data.login + output['$password:'] = record_data.password + output['$link:'] = record_data.link + + if record_data.totp: + output['Totp:'] = record_data.totp + + if record_data.attachments: + output['Attachments:'] = [{ + 'Id': a.get('id'), + 'Name': a.get('name'), + 'Size': a.get('size') + } for a in record_data.attachments] + + if record_data.custom: + output['Custom fields:'] = [vault_extensions.extract_typed_field(field) for field in record_data.custom] + + def _add_typed_record_json_fields(self, record_data: vault_record.TypedRecord, output: dict): + """Add typed record specific fields to JSON output.""" + output['Notes:'] = record_data.notes + output['Fields:'] = [vault_extensions.extract_typed_field(field) for field in record_data.fields] + output['Custom:'] = [vault_extensions.extract_typed_field(field) for field in record_data.custom] + + def _add_file_record_json_fields(self, record_data: vault_record.FileRecord, output: dict): + """Add file record specific fields to JSON output.""" + output['Name:'] = record_data.file_name + output['MIME Type:'] = record_data.mime_type + output['Size:'] = record_data.size + + def _add_share_info_to_json(self, vault: vault_data.VaultData, uid: str, output: dict): + """Add share information to JSON output.""" + share_infos = share_utils.get_record_shares(vault=vault, record_uids=[uid]) + if share_infos and len(share_infos) > 0: + share_info = share_infos[0] + shares = share_info.get('shares', {}) + record_shares = shares.get('user_permissions') + folder_shares = shares.get('shared_folder_permissions') + + if record_shares: + output['User Shares:'] = record_shares + if folder_shares: + output['Shared Folders:'] = folder_shares + + def _display_shared_folder_json(self, vault: vault_data.VaultData, uid: str): + """Display shared folder information in JSON format.""" + shared_folder = vault.vault_data.load_shared_folder(shared_folder_uid=uid) + output = { + 'Shared Folder UID:': uid, + 'Name:': shared_folder.name, + 'Default Manage Records:': shared_folder.default_manage_records, + 'Default Manage Users:': shared_folder.default_manage_users, + 'Default Can Edit:': shared_folder.default_can_edit, + 'Default Can Share:': shared_folder.default_can_share + } + + if len(shared_folder.record_permissions) > 0: + output['Record Permissions:'] = [{ + 'record_uid': r.record_uid, + 'can_edit': r.can_edit, + 'can_share': r.can_share + } for r in shared_folder.record_permissions] + + if len(shared_folder.user_permissions) > 0: + output['User Permissions:'] = [{ + 'user_uid': u.user_uid, + 'name': u.name, + 'user_type': u.user_type, + 'manage_records': u.manage_records, + 'manage_users': u.manage_users + } for u in shared_folder.user_permissions] + + logger.info(json.dumps(output, indent=2)) + + def _display_folder_json(self, vault: vault_data.VaultData, uid: str): + """Display folder information in JSON format.""" + folder = vault.vault_data.get_folder(folder_uid=uid) + output = { + 'Folder UID:': uid, + 'Parent Folder UID:': folder.parent_uid, + 'Folder Type:': folder.folder_type, + 'Name:': folder.name + } + logger.info(json.dumps(output, indent=2)) + + def _display_team_json(self, vault: vault_data.VaultData, uid: str): + """Display team information in JSON format.""" + team = vault.vault_data.get_team(team_uid=uid) + output = { + 'Team UID:': uid, + 'Name:': team.name + } + logger.info(json.dumps(output, indent=2)) + + def _display_record_detail(self, vault: vault_data.VaultData, uid: str, unmask: bool): + """Display record information in detailed format.""" + record = vault.vault_data.get_record(record_uid=uid) + record_data = vault.vault_data.load_record(record_uid=uid) + + self._display_record_header(record, uid) + + if isinstance(record_data, vault_record.PasswordRecord): + self._display_password_record_detail(record_data, unmask) + elif isinstance(record_data, vault_record.TypedRecord): + self._display_typed_record_detail(record_data, unmask) + elif isinstance(record_data, vault_record.FileRecord): + self._display_file_record_detail(record_data) + + self._display_share_information(vault, uid) + self._display_share_admins(vault, uid) + + def _display_record_header(self, record, uid: str): + """Display the header information for a record.""" + logger.info('') + logger.info('{0:>20s}: {1:<20s}'.format('UID', uid)) + logger.info('{0:>20s}: {1:<20s}'.format('Type', record.record_type or '')) + if record.title: + logger.info('{0:>20s}: {1:<20s}'.format('Title', record.title)) + + def _display_password_record_detail(self, record_data: vault_record.PasswordRecord, unmask: bool): + """Display password record details.""" + if record_data.login: + logger.info('{0:>20s}: {1:<20s}'.format('Login', record_data.login)) + if record_data.password: + password_display = record_data.password if unmask else '********' + logger.info('{0:>20s}: {1:<20s}'.format('Password', password_display)) + if record_data.link: + logger.info('{0:>20s}: {1:<20s}'.format('URL', record_data.link)) + + self._display_custom_fields(record_data.custom) + self._display_notes(record_data.notes) + self._display_attachments(record_data.attachments) + self._display_totp(record_data.totp, unmask) + + def _display_typed_record_detail(self, record_data: vault_record.TypedRecord, unmask: bool): + """Display typed record details.""" + # Display typed record fields + for field in record_data.fields: + if field.value: + field_value = field.get_default_value() + if self._is_sensitive_field_type(field.type) and not unmask: + field_value = '********' + logger.info('{0:>20s}: {1:<20s}'.format(field.type, str(field_value))) + + # Display custom fields + for field in record_data.custom: + if field.value: + field_value = field.get_default_value() + if self._is_sensitive_field_type(field.type) and not unmask: + field_value = '********' + logger.info('{0:>20s}: {1:<20s}'.format(field.type, str(field_value))) + + self._display_notes(record_data.notes) + + def _display_file_record_detail(self, record_data: vault_record.FileRecord): + """Display file record details.""" + logger.info('{0:>20s}: {1:<20s}'.format('File Name', record_data.file_name)) + logger.info('{0:>20s}: {1:<20s}'.format('MIME Type', record_data.mime_type)) + logger.info('{0:>20s}: {1:<20s}'.format('Size', str(record_data.size))) + + def _display_custom_fields(self, custom_fields): + """Display custom fields.""" + if custom_fields: + for c in custom_fields: + logger.info('{0:>20s}: {1:21s} {1}'.format('Notes:' if i == 0 else '', lines[i].strip())) + + def _display_attachments(self, attachments): + """Display attachment information.""" + if attachments: + for i in range(len(attachments)): + atta = attachments[i] + size = atta.size or 0 + scale = 'b' + if size > 0: + if size > 1000: + size = size / 1024 + scale = 'Kb' + if size > 1000: + size = size / 1024 + scale = 'Mb' + if size > 1000: + size = size / 1024 + scale = 'Gb' + sz = '{0:.2f}'.format(size).rstrip('0').rstrip('.') + logger.info('{0:>21s} {1:<20s} {2:>6s}{3:<2s} {4:>6s}: {5}'.format( + 'Attachments:' if i == 0 else '', atta.title or atta.name, sz, scale, 'ID', atta.id)) + + def _display_totp(self, totp: str, unmask: bool): + """Display TOTP information.""" + if totp: + totp_display = totp if unmask else '********' + logger.info('{0:>20s}: {1}'.format('TOTP URL', totp_display)) + code, remain, _ = record_utils.get_totp_code(totp) + if code: + logger.info('{0:>20s}: {1:<20s} valid for {2} sec'.format('Two Factor Code', code, remain)) + + def _display_share_information(self, vault: vault_data.VaultData, uid: str): + """Display share information for a record.""" + share_infos = share_utils.get_record_shares(vault=vault, record_uids=[uid]) + if not share_infos or len(share_infos) == 0: + return + + share_info = share_infos[0] + shares = share_info.get('shares', {}) + record_shares = shares.get('user_permissions') + folder_shares = shares.get('shared_folder_permissions') + + if record_shares: + self._display_user_permissions(record_shares) + if folder_shares: + self._display_folder_permissions(folder_shares) + + def _display_user_permissions(self, record_shares): + """Display user permissions.""" + logger.info('') + logger.info('User Permissions:') + for user in record_shares: + logger.info('') + if 'username' in user: + logger.info('User: ' + user['username']) + if 'user_uid' in user: + logger.info('User UID: ' + user['user_uid']) + elif 'accountUid' in user: + logger.info('User UID: ' + user['accountUid']) + + # Handle both possible spellings of sharable/shareable + shareable = user.get('sharable') or user.get('shareable', False) + + logger.info('Shareable: ' + ('Yes' if shareable else 'No')) + logger.info('Read-Only: ' + ('Yes' if not shareable else 'No')) + logger.info('') + + def _display_folder_permissions(self, folder_shares): + """Display folder permissions.""" + logger.info('') + logger.info('Shared Folder Permissions:') + for sf in folder_shares: + logger.info('') + if 'shared_folder_uid' in sf: + logger.info('Shared Folder UID: ' + sf['shared_folder_uid']) + if 'user_uid' in sf: + logger.info('User UID: ' + sf['user_uid']) + elif 'accountUid' in sf: + logger.info('User UID: ' + sf['accountUid']) + + if sf.get('manage_users', False) is True: + logger.info('Manage Users: True') + if sf.get('manage_records', False) is True: + logger.info('Manage Records: True') + if sf.get('can_edit', False) is True: + logger.info('Can Edit: True') + if sf.get('can_share', False) is True: + logger.info('Can Share: True') + logger.info('') + + def _display_share_admins(self, vault: vault_data.VaultData, uid: str): + """Display share admins for a record.""" + admins = record_utils.get_share_admins_for_record(vault=vault, record_uid=uid) + if admins: + logger.info('') + logger.info('Share Admins:') + for admin in admins: + logger.info(admin) + + def _display_shared_folder_detail(self, vault: vault_data.VaultData, uid: str): + """Display shared folder information in detailed format.""" + shared_folder = vault.vault_data.load_shared_folder(shared_folder_uid=uid) + logger.info('') + logger.info('{0:>25s}: {1:<20s}'.format('Shared Folder UID', shared_folder.shared_folder_uid)) + logger.info('{0:>25s}: {1}'.format('Name', shared_folder.name)) + logger.info('{0:>25s}: {1}'.format('Default Manage Records', shared_folder.default_manage_records)) + logger.info('{0:>25s}: {1}'.format('Default Manage Users', shared_folder.default_manage_users)) + logger.info('{0:>25s}: {1}'.format('Default Can Edit', shared_folder.default_can_edit)) + logger.info('{0:>25s}: {1}'.format('Default Can Share', shared_folder.default_can_share)) + + if len(shared_folder.record_permissions) > 0: + logger.info('') + logger.info('{0:>25s}:'.format('Record Permissions')) + for r in shared_folder.record_permissions: + logger.info('{0:>25s}: {1}'.format(r.record_uid, folder_utils.record_permission_to_string({ + 'can_edit': r.can_edit, + 'can_share': r.can_share + }))) + + if len(shared_folder.user_permissions) > 0: + logger.info('') + logger.info('{0:>25s}:'.format('User Permissions')) + for u in shared_folder.user_permissions: + logger.info('{0:>25s}: {1}'.format(u.name or u.user_uid, folder_utils.user_permission_to_string({ + 'manage_users': u.manage_users, + 'manage_records': u.manage_records + }))) + + logger.info('') + + def _display_folder_detail(self, vault: vault_data.VaultData, uid: str): + """Display folder information in detailed format.""" + folder = vault.vault_data.get_folder(folder_uid=uid) + logger.info('') + logger.info('{0:>20s}: {1:<20s}'.format('Folder UID', folder.folder_uid)) + logger.info('{0:>20s}: {1:<20s}'.format('Folder Type', folder.folder_type)) + logger.info('{0:>20s}: {1}'.format('Name', folder.name)) + if folder.parent_uid: + logger.info('{0:>20s}: {1:<20s}'.format('Parent Folder UID', folder.parent_uid)) + if folder.folder_type == 'shared_folder_folder': + logger.info('{0:>20s}: {1:<20s}'.format('Shared Folder UID', folder.folder_scope_uid)) + + def _display_team_detail(self, vault: vault_data.VaultData, uid: str): + """Display team information in detailed format.""" + team = vault.vault_data.get_team(team_uid=uid) + logger.info('') + logger.info('{0:>20s}: {1:<20s}'.format('Team UID', team.team_uid)) + logger.info('{0:>20s}: {1}'.format('Name', team.name)) + logger.info('{0:>20s}: {1}'.format('Restrict Edit', team.restrict_edit)) + logger.info('{0:>20s}: {1}'.format('Restrict View', team.restrict_view)) + logger.info('{0:>20s}: {1}'.format('Restrict Share', team.restrict_share)) + logger.info('') + + def _display_record_password(self, vault: vault_data.VaultData, uid: str): + """Display only the password field of a record.""" + record_data = vault.vault_data.load_record(record_uid=uid) + if isinstance(record_data, vault_record.PasswordRecord): + logger.info(record_data.password) + elif isinstance(record_data, vault_record.TypedRecord): + password_field = record_data.get_typed_field('password') + if password_field and password_field.value: + logger.info(password_field.get_default_value(str)) + else: + logger.info('No password field found in this record type') + + def _display_record_fields(self, vault: vault_data.VaultData, uid: str, unmask: bool): + """Display record fields in JSON format.""" + record = vault.vault_data.get_record(record_uid=uid) + record_data = vault.vault_data.load_record(record_uid=uid) + + fields = [] + normalize_titles = {} + + # Get share information + share_infos = share_utils.get_record_shares(vault=vault, record_uids=[uid]) + record_shares = [] + folder_shares = [] + if share_infos and len(share_infos) > 0: + share_info = share_infos[0] + shares = share_info.get('shares', {}) + record_shares = shares.get('user_permissions', []) + folder_shares = shares.get('shared_folder_permissions', []) + + self._add_record_properties_to_fields(record, record_shares, folder_shares, fields, normalize_titles) + + self._add_typed_fields_to_output(record_data, unmask, fields, normalize_titles) + + if record_data.notes: + field = { + 'name': 'Notes', + 'value': record_data.notes, + } + fields.append(field) + + logger.info(json.dumps(fields, indent=2)) + + def _add_record_properties_to_fields(self, record, record_shares, folder_shares, fields, normalize_titles): + """Add record properties to the fields list.""" + record_props = { + 'title': record.title, + 'record_uid': record.record_uid, + 'revision': record.revision, + 'version': record.version, + 'shared': True if record_shares or folder_shares else False, + } + for prop_name, prop_value in record_props.items(): + normalize_titles[prop_name.lower()] = prop_name + key = prop_name + field = { + 'name': key, + 'value': prop_value, + } + fields.append(field) + + def _add_typed_fields_to_output(self, record_data, unmask: bool, fields, normalize_titles): + """Add typed fields to the output.""" + for field in record_data.get_typed_fields(): + key = field.label or field.type + if key in normalize_titles: + key = normalize_titles[key.lower()] + normalize_titles[key.lower()] = key + var_value = '' + if field.value and (unmask or not self._is_sensitive_field_type(field.type)): + var_value = field.get_external_value() + elif field.value: + var_value = '********' + + field_obj = { + 'name': key, + 'value': var_value, + } + fields.append(field_obj) + + def _is_sensitive_field_type(self, field_type: str) -> bool: + """Check if a field type is considered sensitive and should be masked.""" + sensitive_types = { + 'password', 'secret', 'otp', 'privateKey', 'pinCode', + 'oneTimeCode', 'keyPair', 'licenseNumber' + } + return field_type in sensitive_types \ No newline at end of file diff --git a/keepercli-package/src/keepercli/helpers/folder_utils.py b/keepercli-package/src/keepercli/helpers/folder_utils.py index 669b7c28..bc88099b 100644 --- a/keepercli-package/src/keepercli/helpers/folder_utils.py +++ b/keepercli-package/src/keepercli/helpers/folder_utils.py @@ -62,3 +62,29 @@ def try_resolve_path(context: KeeperParams, path: str) -> Tuple[vault_types.Fold # The second is the final component of the path we're passed as an argument to this function. It could be a record, or # a not-yet-existent directory. return folder, path + + +def user_permission_to_string(permission): + if isinstance(permission, dict): + manage_users = permission.get('manage_users', False) + manage_records = permission.get('manage_records', False) + if manage_users and manage_records: + return 'Can Manage Users & Records' + if not manage_users and not manage_records: + return 'No Folder Permissions' + if manage_users: + return 'Can Manage Users' + return 'Can Manage Records' + + +def record_permission_to_string(permission): + if isinstance(permission, dict): + can_edit = permission.get('can_edit', False) + can_share = permission.get('can_share', False) + if can_edit and can_share: + return 'Can Edit & Share' + if not can_edit and not can_share: + return 'Read Only' + if can_edit: + return 'Can Edit' + return 'Can Share' \ No newline at end of file diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index 28c01e04..84c2eaa1 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -1,10 +1,28 @@ +from base64 import b32decode +import datetime import fnmatch +import hashlib +import hmac import re -from typing import Optional, Iterator, List +from datetime import timedelta +from typing import Iterator, List, Optional +from urllib import parse +from urllib.parse import urlunparse + +from keepersdk import crypto, utils +from keepersdk.proto.APIRequest_pb2 import AddExternalShareRequest, Device +from keepersdk.proto.enterprise_pb2 import GetSharingAdminsRequest, GetSharingAdminsResponse +from keepersdk.vault import vault_online, vault_record, vault_types, vault_utils -from keepersdk.vault import vault_record, vault_utils, vault_types -from . import folder_utils from ..params import KeeperParams +from .. import api +from . import folder_utils + +logger = api.get_logger() + +GET_SHARE_ADMINS = 'enterprise/get_sharing_admins' +EXTERNAL_SHARE_ADD_URL = 'vault/external_share_add' +KEEPER_SECRETS_MANAGER_CLIENT_ID = 'KEEPER_SECRETS_MANAGER_CLIENT_ID' def try_resolve_single_record(record_name: Optional[str], context: KeeperParams) -> Optional[vault_record.KeeperRecordInfo]: @@ -23,6 +41,8 @@ def try_resolve_single_record(record_name: Optional[str], context: KeeperParams) record_info = context.vault.vault_data.get_record(record_uid) if record_info and record_info.title.casefold() == name: return record_info + return None + def resolve_records(pattern: str, context: KeeperParams, *, recursive: bool=False) -> Iterator[str]: assert context.vault is not None @@ -56,4 +76,87 @@ def add_folder(f: vault_types.Folder) -> None: def default_confirm(prompt: str) -> bool: - return input(f"{prompt} (y/n): ").strip().lower() == 'y' \ No newline at end of file + return input(f"{prompt} (y/n): ").strip().lower() == 'y' + + +def process_external_share(context: KeeperParams, expiration_period: timedelta, + record: vault_record.PasswordRecord | vault_record.TypedRecord) -> str: + + vault = context.vault + record_uid = record.record_uid + record_key = vault.vault_data.get_record_key(record_uid=record_uid) + client_key = utils.generate_aes_key() + client_id = crypto.hmac_sha512(client_key, KEEPER_SECRETS_MANAGER_CLIENT_ID.encode()) + + request = AddExternalShareRequest() + request.recordUid = utils.base64_url_decode(record_uid) + request.encryptedRecordKey = crypto.encrypt_aes_v2(record_key, client_key) + request.clientId = client_id + request.accessExpireOn = utils.current_milli_time() + int(expiration_period.total_seconds() * 1000) + request.isSelfDestruct = True + + vault.keeper_auth.execute_auth_rest( + rest_endpoint=EXTERNAL_SHARE_ADD_URL, + request=request, + response_type=Device + ) + + url = urlunparse(( + 'https', + context.server, + '/vault/share', + None, + None, + utils.base64_url_encode(client_key) + )) + return url + + +def get_totp_code(url, offset=None): + comp = parse.urlparse(url) + if comp.scheme == 'otpauth': + params = dict(parse.parse_qsl(comp.query)) + + secret = params.get('secret') + algorithm = params.get('algorithm', 'SHA1') + digits = int(params['digits']) if 'digits' in params else 6 + period = int(params['period']) if 'period' in params else 30 + if secret: + tm_base = int(datetime.datetime.now().timestamp()) + tm = tm_base / period + if isinstance(offset, int): + tm += offset + alg = algorithm.lower() + if alg in hashlib.__dict__: + reminder = len(secret) % 8 + if reminder in {2, 4, 5, 7}: + padding = '=' * (8 - reminder) + secret += padding + key = bytes(b32decode(secret)) + msg = int(tm).to_bytes(8, byteorder='big') + hash = hashlib.__dict__[alg] + hm = hmac.new(key, msg=msg, digestmod=hash) + digest = hm.digest() + offset = digest[-1] & 0x0f + base = bytearray(digest[offset:offset + 4]) + base[0] = base[0] & 0x7f + code_int = int.from_bytes(base, byteorder='big') + code = str(code_int % (10 ** digits)) + if len(code) < digits: + code = code.rjust(digits, '0') + return code, period - (tm_base % period), period + else: + raise Exception(f'Unsupported hash algorithm: {algorithm}') + + +def get_share_admins_for_record(vault: vault_online.VaultOnline, record_uid: str): + try: + request = GetSharingAdminsRequest() + request.recordUid = utils.base64_url_decode(record_uid) + response = vault.keeper_auth.execute_auth_rest(rest_endpoint=GET_SHARE_ADMINS, request=request, response_type= GetSharingAdminsResponse) + admins = [x.email for x in response.userProfileExts if x.isShareAdminForRequestedObject] + except Exception as e: + logger.debug(e) + return + + return admins \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index a4211ed2..8cd3ae11 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -39,6 +39,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') commands.register_command('record-update', record_edit.RecordUpdateCommand(), base.CommandScope.Vault, 'ru') commands.register_command('rm', record_edit.RecordDeleteCommand(), base.CommandScope.Vault) + commands.register_command('get', record_edit.RecordGetCommand(), base.CommandScope.Vault) commands.register_command('delete-attachment', record_edit.RecordDeleteAttachmentCommand(), base.CommandScope.Vault) commands.register_command('download-attachment', record_edit.RecordDownloadAttachmentCommand(), base.CommandScope.Vault, 'da') commands.register_command('upload-attachment', record_edit.RecordUploadAttachmentCommand(), base.CommandScope.Vault, 'ua') diff --git a/keepersdk-package/src/keepersdk/vault/vault_record.py b/keepersdk-package/src/keepersdk/vault/vault_record.py index 8762b50f..dd119ddb 100644 --- a/keepersdk-package/src/keepersdk/vault/vault_record.py +++ b/keepersdk-package/src/keepersdk/vault/vault_record.py @@ -203,6 +203,10 @@ def enumerate_fields(self) -> Iterable[Tuple[str, str, Any]]: for cf in self.custom: yield '', cf.name, cf.value + def get_typed_fields(self) -> List[TypedField]: + """Return all typed fields. For PasswordRecord, this returns an empty list as it uses legacy fields.""" + return [] + class TypedField(record_types.ITypedField): def __init__(self): @@ -314,6 +318,10 @@ def enumerate_fields(self): if value: yield field.type, field.label or '', value + def get_typed_fields(self) -> List[TypedField]: + """Return all typed fields (both main fields and custom fields).""" + return list(itertools.chain(self.fields, self.custom)) + class FileRecord(KeeperRecord): def __init__(self) -> None: @@ -338,6 +346,10 @@ def enumerate_fields(self): yield 'file_name', '', self.file_name yield 'mime_type', '', self.mime_type + def get_typed_fields(self) -> List[TypedField]: + """Return all typed fields. For FileRecord, this returns an empty list as it uses basic fields.""" + return [] + class ApplicationRecord(KeeperRecord): def __init__(self) -> None: @@ -350,3 +362,7 @@ def version(self): def load_record_data(self, data, extra=None): self.title = sanitize_str_field_value(data.get('title')) self.app_type = sanitize_str_field_value(data.get('type')) + + def get_typed_fields(self) -> List[TypedField]: + """Return all typed fields. For ApplicationRecord, this returns an empty list as it uses basic fields.""" + return [] From 513a02d29535e6b51cd597b588b38590e965aa0b Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 4 Aug 2025 14:18:01 +0530 Subject: [PATCH 17/44] Used enumerate_fields --- .../src/keepercli/commands/record_edit.py | 12 ++++++------ .../src/keepersdk/vault/vault_record.py | 16 ---------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index 24192cad..f3273c9b 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -1623,15 +1623,15 @@ def _add_record_properties_to_fields(self, record, record_shares, folder_shares, def _add_typed_fields_to_output(self, record_data, unmask: bool, fields, normalize_titles): """Add typed fields to the output.""" - for field in record_data.get_typed_fields(): - key = field.label or field.type + for field_type, field_label, field_value in record_data.enumerate_fields(): + key = field_label or field_type if key in normalize_titles: key = normalize_titles[key.lower()] normalize_titles[key.lower()] = key - var_value = '' - if field.value and (unmask or not self._is_sensitive_field_type(field.type)): - var_value = field.get_external_value() - elif field.value: + + if unmask or not self._is_sensitive_field_type(field_type): + var_value = field_value + else: var_value = '********' field_obj = { diff --git a/keepersdk-package/src/keepersdk/vault/vault_record.py b/keepersdk-package/src/keepersdk/vault/vault_record.py index dd119ddb..8762b50f 100644 --- a/keepersdk-package/src/keepersdk/vault/vault_record.py +++ b/keepersdk-package/src/keepersdk/vault/vault_record.py @@ -203,10 +203,6 @@ def enumerate_fields(self) -> Iterable[Tuple[str, str, Any]]: for cf in self.custom: yield '', cf.name, cf.value - def get_typed_fields(self) -> List[TypedField]: - """Return all typed fields. For PasswordRecord, this returns an empty list as it uses legacy fields.""" - return [] - class TypedField(record_types.ITypedField): def __init__(self): @@ -318,10 +314,6 @@ def enumerate_fields(self): if value: yield field.type, field.label or '', value - def get_typed_fields(self) -> List[TypedField]: - """Return all typed fields (both main fields and custom fields).""" - return list(itertools.chain(self.fields, self.custom)) - class FileRecord(KeeperRecord): def __init__(self) -> None: @@ -346,10 +338,6 @@ def enumerate_fields(self): yield 'file_name', '', self.file_name yield 'mime_type', '', self.mime_type - def get_typed_fields(self) -> List[TypedField]: - """Return all typed fields. For FileRecord, this returns an empty list as it uses basic fields.""" - return [] - class ApplicationRecord(KeeperRecord): def __init__(self) -> None: @@ -362,7 +350,3 @@ def version(self): def load_record_data(self, data, extra=None): self.title = sanitize_str_field_value(data.get('title')) self.app_type = sanitize_str_field_value(data.get('type')) - - def get_typed_fields(self) -> List[TypedField]: - """Return all typed fields. For ApplicationRecord, this returns an empty list as it uses basic fields.""" - return [] From 41a6a0859829eb3fafec83b3d55a94a994104e81 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 4 Aug 2025 18:09:12 +0530 Subject: [PATCH 18/44] Added uid flags --- .../src/keepercli/commands/record_edit.py | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index f3273c9b..75089b80 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -1074,8 +1074,20 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser): help='output format as detail, json, password, fields' ) parser.add_argument( - 'uid', type=str, action='store', - help='UID or title to search for' + '-f', '--folder', dest='folder', action='store', + help='folder UID or title to search for' + ) + parser.add_argument( + '-t', '--team', dest='team', action='store', + help='team UID or title to search for' + ) + parser.add_argument( + '-r', '--record', dest='record', action='store', + help='record UID or title to search for' + ) + parser.add_argument( + 'uid', type=str, action='store', nargs='?', default=None, + help='UID or title to search for (optional when using -f, -t, or -r flags)' ) def execute(self, context: KeeperParams, **kwargs): @@ -1085,11 +1097,37 @@ def execute(self, context: KeeperParams, **kwargs): uid = kwargs.get('uid') output_format = kwargs.get('format', 'detail') unmask = kwargs.get('unmask', False) + folder = kwargs.get('folder') + team = kwargs.get('team') + record = kwargs.get('record') - if not uid: - raise base.CommandError('UID parameter is required') - - target_object = self._find_target_object(context.vault, uid) + if folder: + shared_folder = self._find_shared_folder(context.vault, folder) + if shared_folder: + target_object = ('shared_folder', shared_folder) + else: + folder = self._find_folder(context.vault, folder) + if folder: + target_object = ('folder', folder) + else: + raise base.CommandError('The given UID or title is not a valid folder') + elif team: + team = self._find_team(context.vault, team) + if team: + target_object = ('team', team) + else: + raise base.CommandError('The given UID or title is not a valid team') + elif record: + record = self._find_record(context.vault, record) + if record: + target_object = ('record', record) + else: + raise base.CommandError('The given UID or title is not a valid record') + elif uid: + target_object = self._find_target_object(context.vault, uid) + else: + raise base.CommandError('Either UID parameter or one of -f, -t, -r flags is required') + if not target_object: raise base.CommandError('The given UID is not a valid Keeper Object') From 1fb50aa9ca43fc19329baf7beabe8aceb0ac3368 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 8 Aug 2025 17:27:01 +0530 Subject: [PATCH 19/44] One-time-share commands --- .../src/keepercli/commands/record_edit.py | 3 +- .../src/keepercli/commands/secrets_manager.py | 6 +- .../keepercli/commands/share_management.py | 397 +++++++++++++++++- .../src/keepercli/helpers/record_utils.py | 35 +- .../src/keepercli/register_commands.py | 3 + .../src/keepersdk/vault/ksm_management.py | 39 +- .../src/keepersdk/vault/record_management.py | 4 + 7 files changed, 460 insertions(+), 27 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index 75089b80..60064df6 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -710,7 +710,8 @@ def execute(self, context: KeeperParams, **kwargs) -> None: else: expiration_period = None expiration_period = timeout_utils.parse_timeout(self_destruct) - if expiration_period.total_seconds() > 182 * 24 * 60 * 60: + SIX_MONTHS_IN_SECONDS = 182 * 24 * 60 * 60 + if expiration_period.total_seconds() > SIX_MONTHS_IN_SECONDS: raise base.CommandError('URL expiration period cannot be greater than 6 months.') url = record_utils.process_external_share(context=context, expiration_period=expiration_period, record=record) expiration_date = datetime.datetime.now() + expiration_period diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index 8500094f..b15c1406 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -41,7 +41,7 @@ class SecretsManagerAppCommand(base.ArgparseCommand): def __init__(self): self.parser = argparse.ArgumentParser( - prog='secrets-manager app', + prog='secrets-manager-app', description='Keeper Secrets Manager (KSM) App Commands', ) SecretsManagerAppCommand.add_arguments_to_parser(self.parser) @@ -56,7 +56,7 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser): help = f"One of: {', '.join(cmd.value for cmd in SecretsManagerCommand)}" ) parser.add_argument( - '--app', '-n', type=str, dest='app', action='store', help='Application Name or UID' + '--app', '-a', type=str, dest='app', action='store', help='Application Name or UID' ) parser.add_argument( '-f', '--force', dest='force', action='store_true', help='Force add or remove app' @@ -83,7 +83,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: return self.get_parser().print_help() if command != SecretsManagerCommand.LIST.value and not uid_or_name: - raise ValueError("Application name or UID is required. Use --name='example' to set it.") + raise ValueError("Application name or UID is required. Use --app='example' to set it.") def list_app(): return self.list_app(vault=vault) diff --git a/keepercli-package/src/keepercli/commands/share_management.py b/keepercli-package/src/keepercli/commands/share_management.py index bbb7c94f..a49abefa 100644 --- a/keepercli-package/src/keepercli/commands/share_management.py +++ b/keepercli-package/src/keepercli/commands/share_management.py @@ -1,4 +1,5 @@ import argparse +import datetime import json import math import re @@ -6,12 +7,12 @@ from typing import Optional from keepersdk import crypto, utils -from keepersdk.proto import folder_pb2, record_pb2 -from keepersdk.vault import vault_online, vault_utils, storage_types +from keepersdk.proto import folder_pb2, record_pb2, APIRequest_pb2 +from keepersdk.vault import ksm_management, vault_online, vault_utils from . import base from .. import api, prompt_utils, constants -from ..helpers import folder_utils, report_utils, share_utils +from ..helpers import folder_utils, record_utils, report_utils, share_utils, timeout_utils from ..params import KeeperParams @@ -19,6 +20,7 @@ class ApiUrl(Enum): SHARE_ADMIN = 'vault/am_i_share_admin' SHARE_UPDATE = 'vault/records_share_update' SHARE_FOLDER_UPDATE = 'vault/shared_folder_update_v3' + REMOVE_EXTERNAL_SHARE = 'vault/external_share_remove' class ShareAction(Enum): @@ -36,12 +38,14 @@ class ManagePermission(Enum): logger = api.get_logger() +TIMESTAMP_MILLISECONDS_FACTOR = 1000 +TRUNCATE_SUFFIX = '...' def set_expiration_fields(obj, expiration): """Set expiration and timerNotificationType fields on proto object if expiration is provided.""" if isinstance(expiration, int): if expiration > 0: - obj.expiration = expiration * 1000 + obj.expiration = expiration * TIMESTAMP_MILLISECONDS_FACTOR obj.timerNotificationType = record_pb2.NOTIFY_OWNER elif expiration < 0: obj.expiration = -1 @@ -1017,3 +1021,388 @@ def send_requests(vault:vault_online.VaultOnline, partitioned_requests): except Exception as kae: logger.error(kae) return + + +class OneTimeShareListCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='share-list', + description='Displays a list of one-time shares for a record', + parents=[base.report_output_parser] + ) + OneTimeShareListCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '-R', '--recursive', dest='recursive', action='store_true', + help='Traverse recursively through subfolders' + ) + parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', help='verbose output.' + ) + parser.add_argument( + '-a', '--all', dest='show_all', action='store_true', help='show all one-time shares including expired.' + ) + parser.add_argument( + 'record', nargs='?', type=str, action='store', help='record/folder path/UID' + ) + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise ValueError('Vault is not initialized.') + + vault = context.vault + + records = kwargs['record'] if 'record' in kwargs else None + if not records: + self.get_parser().print_help() + return + if isinstance(records, str): + records = [records] + + record_uids = self._resolve_record_uids(context, vault, records, kwargs.get('recursive', False)) + if not record_uids: + raise base.CommandError('one-time-share', 'No records found') + + applications = self._get_applications(vault, record_uids) + table_data = self._build_share_table(applications, kwargs) + + return self._format_output(table_data, kwargs) + + def _resolve_record_uids(self, context: KeeperParams, vault, records: list, recursive: bool) -> set: + """Resolve record names/paths to UIDs.""" + record_uids = set() + + for name in records: + record_uid = None + folder_uid = None + if name in vault.vault_data._records: + record_uid = name + elif name in vault.vault_data._folders: + folder_uid = name + else: + rs = folder_utils.try_resolve_path(context, name) + if rs is not None: + folder, r_name = rs + if r_name: + f_uid = folder.folder_uid or '' + if f_uid in vault.vault_data._folders: + for uid in folder.records: + rec = vault.vault_data.get_record(record_uid=uid) + if rec and rec.version in (2, 3) and rec.title.lower() == r_name.lower(): + record_uid = uid + break + else: + folder_uid = folder.folder_uid or '' + + if record_uid is not None: + record_uids.add(record_uid) + elif folder_uid is not None: + self._add_folder_records(vault, folder_uid, record_uids, recursive) + + return record_uids + + def _add_folder_records(self, vault, folder_uid: str, record_uids: set, recursive: bool): + """Add records from a folder to the record_uids set.""" + def on_folder(f): + f_uid = f.folder_uid or '' + if f_uid in vault.vault_data._folders: + folder = vault.vault_data.get_folder(folder_uid=f_uid) + recs = folder.records + if recs: + record_uids.update(recs) + + folder = vault.vault_data.get_folder(folder_uid=folder_uid) + if recursive: + vault_utils.traverse_folder_tree(vault, folder_uid, on_folder) + else: + on_folder(folder) + + def _get_applications(self, vault, record_uids: set): + """Get application info for the given record UIDs.""" + r_uids = list(record_uids) + MAX_BATCH_SIZE = 1000 + if len(r_uids) >= MAX_BATCH_SIZE: + logger.info('Trimming result to %d records', MAX_BATCH_SIZE) + r_uids = r_uids[:MAX_BATCH_SIZE - 1] + return ksm_management.get_app_info(vault=vault, app_uid=r_uids) + + def _build_share_table(self, applications, kwargs): + """Build table data from applications.""" + show_all = kwargs.get('show_all', False) + verbose = kwargs.get('verbose', False) + now = utils.current_milli_time() + + fields = ['record_uid', 'share_link_name', 'share_link_id', 'generated', 'opened', 'expires'] + if show_all: + fields.append('status') + + table = [] + output_format = kwargs.get('format') + + for app_info in applications: + if not app_info.isExternalShare: + continue + + for client in app_info.clients: + if not show_all and now > client.accessExpireOn: + continue + + link = self._create_share_link_data(app_info, client, verbose, output_format, now) + table.append([link.get(x, '') for x in fields]) + + return table, fields + + def _create_share_link_data(self, app_info, client, verbose: bool, output_format: str, now: int): + """Create share link data dictionary.""" + link = { + 'record_uid': utils.base64_url_encode(app_info.appRecordUid), + 'name': client.id, + 'share_link_id': utils.base64_url_encode(client.clientId), + 'generated': datetime.datetime.fromtimestamp(client.createdOn / TIMESTAMP_MILLISECONDS_FACTOR), + 'expires': datetime.datetime.fromtimestamp(client.accessExpireOn / TIMESTAMP_MILLISECONDS_FACTOR), + } + + TRUNCATE_LENGTH = 20 + if output_format == 'table' and not verbose: + link['share_link_id'] = utils.base64_url_encode(client.clientId)[:TRUNCATE_LENGTH] + TRUNCATE_SUFFIX + else: + link['share_link_id'] = utils.base64_url_encode(client.clientId) + + if client.firstAccess > 0: + link['opened'] = datetime.datetime.fromtimestamp(client.firstAccess / TIMESTAMP_MILLISECONDS_FACTOR) + link['accessed'] = datetime.datetime.fromtimestamp(client.lastAccess / TIMESTAMP_MILLISECONDS_FACTOR) + + if now > client.accessExpireOn: + link['status'] = 'Expired' + elif client.firstAccess > 0: + link['status'] = 'Opened' + else: + link['status'] = 'Generated' + + return link + + def _format_output(self, table_data, kwargs): + """Format and return the output.""" + table, fields = table_data + output_format = kwargs.get('format') + + if output_format == 'table': + fields = [report_utils.field_to_title(x) for x in fields] + + return report_utils.dump_report_data(table, fields, fmt=output_format, filename=kwargs.get('output')) + + +class OneTimeShareCreateCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='share-create', + description='Creates one-time share URL for a record' + ) + OneTimeShareCreateCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '--output', dest='output', choices=['clipboard', 'stdout'], action='store', + help='URL output destination' + ) + parser.add_argument( + '--name', dest='share_name', action='store', help='one-time share URL name' + ) + parser.add_argument( + '-e', '--expire', dest='expire', action='store', metavar='[(mi)nutes|(h)ours|(d)ays]', + help='time period record share URL is valid.' + ) + parser.add_argument( + '--editable', dest='is_editable', action='store_true', help='allow the user to edit the shared record' + ) + parser.add_argument( + 'record', nargs='?', type=str, action='store', help='record path or UID. Can be repeated' + ) + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise ValueError('Vault is not initialized.') + + vault = context.vault + + record_names = kwargs.get('record') + period_str = kwargs.get('expire') + name = kwargs.get('share_name', '') + is_editable = kwargs.get('is_editable', False) + if isinstance(record_names, str): + record_names = [record_names] + if not record_names: + self.get_parser().print_help() + return None + if not period_str: + logger.warning('URL expiration period parameter \"--expire\" is required.') + self.get_parser().print_help() + return None + + period = self._validate_and_parse_expiration(period_str) + + urls = self._create_share_urls(context, vault, record_names, period, name, is_editable) + + return self._handle_output(context, urls, kwargs) + + def _validate_and_parse_expiration(self, period_str): + """Validate and parse the expiration period.""" + period = timeout_utils.parse_timeout(period_str) + SIX_MONTHS_IN_SECONDS = 182 * 24 * 60 * 60 + if period.total_seconds() > SIX_MONTHS_IN_SECONDS: + raise base.CommandError('one-time-share', 'URL expiration period cannot be greater than 6 months.') + return period + + def _create_share_urls(self, context: KeeperParams, vault, record_names: list, period, name: str, is_editable: bool): + """Create share URLs for the given records.""" + urls = {} + for record_name in record_names: + record_uid = record_utils.resolve_record(context=context, name=record_name) + record = vault.vault_data.load_record(record_uid=record_uid) + url = record_utils.process_external_share( + context=context, expiration_period=period, record=record, name=name, is_editable=is_editable, is_self_destruct=False + ) + urls[record_uid] = str(url) + return urls + + def _handle_output(self, context: KeeperParams, urls: dict, kwargs): + """Handle different output formats for the URLs.""" + if context.batch_mode: + return '\n'.join(urls.values()) + + output = kwargs.get('output') or '' + if len(urls) > 1 and not output: + output = 'stdout' + + if output == 'clipboard' and len(urls) == 1: + return self._copy_to_clipboard(urls) + elif output == 'stdout': + return self._output_to_stdout(urls) + else: + return '\n'.join(urls.values()) + + def _copy_to_clipboard(self, urls: dict): + """Copy URL to clipboard.""" + import pyperclip + url = next(iter(urls.values())) + pyperclip.copy(url) + logger.info('One-Time record share URL is copied to clipboard') + return None + + def _output_to_stdout(self, urls: dict): + """Output URLs to stdout in table format.""" + table = [list(x) for x in urls.items()] + headers = ['Record UID', 'URL'] + report_utils.dump_report_data(table, headers) + return None + + +class OneTimeShareRemoveCommand(base.ArgparseCommand): + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog = 'share-remove', + description= 'Removes one-time share URL for a record' + ) + OneTimeShareRemoveCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + 'record', nargs='?', type=str, action='store', help='record path or UID' + ) + parser.add_argument( + 'share', nargs='?', type=str, action='store', help='one-time share name or ID' + ) + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise ValueError('Vault is not initialized.') + + vault = context.vault + + record_name = kwargs.get('record') + if not record_name: + self.get_parser().print_help() + return + + record_uid = record_utils.resolve_record(context=context, name=record_name) + applications = ksm_management.get_app_info(vault=vault, app_uid=record_uid) + + if len(applications) == 0: + logger.info('There are no one-time shares for record \"%s\"', record_name) + return + + share_name = kwargs.get('share') + if not share_name: + self.get_parser().print_help() + return + + client_id = self._find_client_id(applications, share_name) + if not client_id: + return + + self._remove_share(vault, record_uid, client_id, share_name, record_name) + + def _find_client_id(self, applications, share_name: str) -> Optional[bytes]: + + cleaned_name = share_name[:-len(TRUNCATE_SUFFIX)] if share_name.endswith(TRUNCATE_SUFFIX) else share_name + cleaned_name_lower = cleaned_name.lower() + + partial_matches = [] + + for app_info in applications: + if not app_info.isExternalShare: + continue + + for client in app_info.clients: + if client.id.lower() == cleaned_name_lower: + return client.clientId + + encoded_client_id = utils.base64_url_encode(client.clientId) + if encoded_client_id == cleaned_name: + return client.clientId + + if encoded_client_id.startswith(cleaned_name): + partial_matches.append(client.clientId) + + return self._resolve_partial_matches(partial_matches, share_name) + + def _resolve_partial_matches(self, partial_matches: list[bytes], original_name: str) -> Optional[bytes]: + """ + Resolve partial matches to a single client ID. + + Args: + partial_matches: List of client IDs that partially match + original_name: Original share name for error reporting + + Returns: + bytes: Single client ID if exactly one match, None otherwise + """ + if not partial_matches: + logger.warning('No one-time share found matching "%s"', original_name) + return None + + if len(partial_matches) == 1: + return partial_matches[0] + + # Multiple matches found + logger.warning('Multiple one-time shares found matching "%s". Please use a more specific identifier.', original_name) + return None + + def _remove_share(self, vault, record_uid: str, client_id: bytes, share_name: str, record_name: str): + """Remove the one-time share.""" + rq = APIRequest_pb2.RemoveAppClientsRequest() + rq.appRecordUid = utils.base64_url_decode(record_uid) + rq.clients.append(client_id) + + vault.keeper_auth.execute_auth_rest(request=rq, rest_endpoint=ApiUrl.REMOVE_EXTERNAL_SHARE.value) + logger.info('One-time share \"%s\" is removed from record \"%s\"', share_name, record_name) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index 84c2eaa1..8b8b8f1d 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -5,6 +5,7 @@ import hmac import re from datetime import timedelta +from token import OP from typing import Iterator, List, Optional from urllib import parse from urllib.parse import urlunparse @@ -14,6 +15,7 @@ from keepersdk.proto.enterprise_pb2 import GetSharingAdminsRequest, GetSharingAdminsResponse from keepersdk.vault import vault_online, vault_record, vault_types, vault_utils +from ..commands.base import CommandError from ..params import KeeperParams from .. import api from . import folder_utils @@ -80,7 +82,9 @@ def default_confirm(prompt: str) -> bool: def process_external_share(context: KeeperParams, expiration_period: timedelta, - record: vault_record.PasswordRecord | vault_record.TypedRecord) -> str: + record: vault_record.PasswordRecord | vault_record.TypedRecord, + name: Optional[str] = None, is_editable: bool = False, + is_self_destruct: Optional[bool] = True) -> str: vault = context.vault record_uid = record.record_uid @@ -93,8 +97,14 @@ def process_external_share(context: KeeperParams, expiration_period: timedelta, request.encryptedRecordKey = crypto.encrypt_aes_v2(record_key, client_key) request.clientId = client_id request.accessExpireOn = utils.current_milli_time() + int(expiration_period.total_seconds() * 1000) - request.isSelfDestruct = True + + if name: + request.id = name + request.isSelfDestruct = is_self_destruct + # TODO: uncomment when proto is updated + # request.isEditable = is_editable + vault.keeper_auth.execute_auth_rest( rest_endpoint=EXTERNAL_SHARE_ADD_URL, request=request, @@ -159,4 +169,23 @@ def get_share_admins_for_record(vault: vault_online.VaultOnline, record_uid: str logger.debug(e) return - return admins \ No newline at end of file + return admins + + +def resolve_record(context: KeeperParams, name: str) -> str: + record_uid = None + vault = context.vault + if name in vault.vault_data._records: + return name + else: + rs = folder_utils.try_resolve_path(context, name) + if rs is not None: + folder, name = rs + if folder is not None and name is not None: + if folder.records: + for uid in folder.records: + r = vault.vault_data.get_record(record_uid=uid) + if r.title.lower() == name.lower(): + return uid + if record_uid is None: + raise CommandError('one-time-share', f'Record not found: {name}') diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 8cd3ae11..d9d5d8ca 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -57,6 +57,9 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('secrets-manager-share', secrets_manager.SecretsManagerShareCommand(), base.CommandScope.Vault) commands.register_command('share-record', share_management.ShareRecordCommand(), base.CommandScope.Vault, 'sr') commands.register_command('share-folder', share_management.ShareFolderCommand(), base.CommandScope.Vault, 'sf') + commands.register_command('share-list', share_management.OneTimeShareListCommand(), base.CommandScope.Vault) + commands.register_command('share-create', share_management.OneTimeShareCreateCommand(), base.CommandScope.Vault) + commands.register_command('share-remove', share_management.OneTimeShareRemoveCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index 2e6eba0c..43e3b81e 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -22,20 +22,21 @@ def list_secrets_manager_apps(vault: vault_online.VaultOnline) -> list[ksm.Secre ) apps_list = [] - for app_summary in response.applicationSummary: - uid = utils.base64_url_encode(app_summary.appRecordUid) - app_record = vault.vault_data.load_record(uid) - name = app_record.title if app_record else '' - last_access = int_to_datetime(app_summary.lastAccess) - secrets_app = ksm.SecretsManagerApp( - name=name, - uid=uid, - records=app_summary.folderRecords, - folders=app_summary.folderShares, - count=app_summary.clientCount, - last_access=last_access - ) - apps_list.append(secrets_app) + if response.applicationSummary: + for app_summary in response.applicationSummary: + uid = utils.base64_url_encode(app_summary.appRecordUid) + app_record = vault.vault_data.load_record(uid) + name = app_record.title if app_record else '' + last_access = int_to_datetime(app_summary.lastAccess) + secrets_app = ksm.SecretsManagerApp( + name=name, + uid=uid, + records=app_summary.folderRecords, + folders=app_summary.folderShares, + count=app_summary.clientCount, + last_access=last_access + ) + apps_list.append(secrets_app) return apps_list @@ -138,9 +139,15 @@ def remove_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str return app.uid -def get_app_info(vault: vault_online.VaultOnline, app_uid): +def get_app_info(vault: vault_online.VaultOnline, app_uid: str | list[str]) -> list: rq = GetAppInfoRequest() - rq.appRecordUid.append(utils.base64_url_decode(app_uid)) + + if isinstance(app_uid, str): + app_uid = [app_uid] + + for uid in app_uid: + rq.appRecordUid.append(utils.base64_url_decode(uid)) + rs = vault.keeper_auth.execute_auth_rest( request=rq, rest_endpoint=URL_GET_APP_INFO_API, diff --git a/keepersdk-package/src/keepersdk/vault/record_management.py b/keepersdk-package/src/keepersdk/vault/record_management.py index 7d068894..517ea450 100644 --- a/keepersdk-package/src/keepersdk/vault/record_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_management.py @@ -18,6 +18,10 @@ def add_record_to_folder(vault: vault_online.VaultOnline, record: Union[Password vault_data = vault.vault_data folder = vault_data.get_folder(folder_uid) if folder_uid else None + + if folder_uid and not folder: + raise ValueError(f'Folder with UID \"{folder_uid}\" not found') + folder_key: Optional[bytes] = None if folder and folder.folder_type in {'shared_folder', 'shared_folder_folder'}: assert folder.folder_scope_uid is not None From d97337bdb43d82e5bad6c2b9479f23509fef2c2c Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 14 Aug 2025 20:51:27 +0530 Subject: [PATCH 20/44] breachwatch scan command --- .../src/keepercli/commands/breachwatch.py | 343 +++++++++++++++--- 1 file changed, 293 insertions(+), 50 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/breachwatch.py b/keepercli-package/src/keepercli/commands/breachwatch.py index b8ba3141..19981d79 100644 --- a/keepercli-package/src/keepercli/commands/breachwatch.py +++ b/keepercli-package/src/keepercli/commands/breachwatch.py @@ -1,20 +1,33 @@ import argparse -from typing import Any, Set +import base64 +import json +from typing import Any, Optional, Set -from keepersdk.proto import client_pb2 -from keepersdk.vault import vault_record +from keepersdk.proto import breachwatch_pb2, client_pb2 +from keepersdk.vault import vault_online, vault_record +from keepersdk import crypto, utils from . import base from .. import api from ..helpers import report_utils, record_utils from ..params import KeeperParams +logger = api.get_logger() + +STATUS_TO_TEXT: dict[int, str] = { + client_pb2.BWStatus.GOOD: "GOOD", + client_pb2.BWStatus.WEAK: "WEAK", + client_pb2.BWStatus.BREACHED: "BREACHED" + } + +UPDATE_BW_RECORD_URL = 'breachwatch/update_record_data' class BreachWatchCommand(base.GroupCommand): def __init__(self): super().__init__('BreachWatch.') self.register_command(BreachWatchListCommand(), 'list', 'l') self.register_command(BreachWatchIgnoreCommand(), 'ignore') + self.register_command(BreachWatchScanCommand(), 'scan') class BreachWatchListCommand(base.ArgparseCommand): def __init__(self): @@ -70,64 +83,294 @@ def __init__(self): def execute(self, context: KeeperParams, **kwargs) -> Any: assert context.vault + vault = context.vault + + # Parse and resolve record names to UIDs + record_names = self._get_record_names(kwargs) + if not record_names: + return + + record_uids = self._resolve_record_uids(record_names, context) + if not record_uids: + return + # Get breached records and their passwords + breached_records = self._get_breached_records(vault) + + # Create breach watch requests + bw_requests = self._create_breach_watch_requests(vault, breached_records, record_uids) + + # Process the requests + if bw_requests: + self._process_breach_watch_requests(vault, bw_requests) + vault.sync_down(force=True) + else: + logger.info("No breach watch requests to process") + + def _get_record_names(self, kwargs: dict) -> list[str]: + """Extract record names from kwargs.""" records = kwargs.get('records') if not records: - return + return [] + if isinstance(records, str): - records = [records] + return [records] + + return records + def _resolve_record_uids(self, record_names: list[str], context: KeeperParams) -> Set[str]: + """Resolve record names to UIDs using the context.""" record_uids: Set[str] = set() - for record_name in records: + for record_name in record_names: record_uids.update(record_utils.resolve_records(record_name, context)) + return record_uids - if len(record_uids) == 0: - return - # TODO - """ - bw_requests: List[breachwatch_pb2.BreachWatchRecordRequest] = [] - for record, password in params.breach_watch.get_records_by_status(params, ['WEAK', 'BREACHED']): - if record.record_uid not in record_uids: + def _get_breached_records(self, vault: vault_online.VaultOnline) -> dict[str, str]: + """Get breached records and their passwords.""" + record_passwords = {} + + for record in vault.vault_data.breach_watch_records(): + if record.status in (client_pb2.BWStatus.WEAK, client_pb2.BWStatus.BREACHED): + password = self._extract_record_password(vault, record.record_uid) + if password: + record_passwords[record.record_uid] = password + + return record_passwords + + def _extract_record_password(self, vault: vault_online.VaultOnline, record_uid: str) -> Optional[str]: + """Extract password from a record.""" + try: + record_data = vault.vault_data.load_record(record_uid) + if isinstance(record_data, vault_record.PasswordRecord): + return record_data.password + elif isinstance(record_data, vault_record.TypedRecord): + return record_data.extract_password() + except Exception as e: + logger.debug(f'Error extracting password from record {record_uid}: {e}') + + return None + + def _create_breach_watch_requests(self, vault: vault_online.VaultOnline, record_passwords: dict[str, str], record_uids: Set[str]) -> list[breachwatch_pb2.BreachWatchRecordRequest]: + """Create breach watch record requests for the given records.""" + bw_requests = [] + + for uid in record_uids: + password = record_passwords.get(uid) + if not password: continue - record_uids.remove(record.record_uid) - bwrq = breachwatch_pb2.BreachWatchRecordRequest() - bwrq.recordUid = utils.base64_url_decode(record.record_uid) - bwrq.breachWatchInfoType = breachwatch_pb2.RECORD - bwrq.updateUserWhoScanned = False - - bw_password = client_pb2.BWPassword() - bw_password.value = password.get('value') - bw_password.resolved = utils.current_milli_time() - bw_password.status = client_pb2.IGNORE - euid = password.get('euid') - if euid: - bw_password.euid = base64.b64decode(euid) - bw_data = client_pb2.BreachWatchData() - bw_data.passwords.append(bw_password) - data = bw_data.SerializeToString() + try: - record_key = params.record_cache[record.record_uid]['record_key_unencrypted'] - bwrq.encryptedData = crypto.encrypt_aes_v2(data, record_key) - except: - logging.warning(f'Record UID "{record.record_uid}" encryption error. Skipping.') + bwrq = self._create_single_breach_watch_request(vault, uid, password) + if bwrq: + bw_requests.append(bwrq) + except Exception as e: + logger.warning(f'Failed to create breach watch request for record {uid}: {e}') continue - bw_requests.append(bwrq) + + return bw_requests + + def _create_single_breach_watch_request(self, vault: vault_online.VaultOnline, record_uid: str, password: str) -> Optional[breachwatch_pb2.BreachWatchRecordRequest]: + """Create a single breach watch record request.""" + # Create the main request + bwrq = breachwatch_pb2.BreachWatchRecordRequest() + bwrq.recordUid = utils.base64_url_decode(record_uid) + bwrq.breachWatchInfoType = breachwatch_pb2.RECORD + bwrq.updateUserWhoScanned = False + + # Create the password object + bw_password = self._create_breach_watch_password(password) + + # Get existing breach watch data if available + euid = self._get_existing_breach_watch_euid(vault, record_uid, password) + if euid: + bw_password.euid = euid + + # Create and encrypt the data + bw_data = client_pb2.BreachWatchData() + bw_data.passwords.append(bw_password) + data = bw_data.SerializeToString() + + try: + record_key = vault.vault_data.get_record_key(record_uid=record_uid) + bwrq.encryptedData = crypto.encrypt_aes_v2(data, record_key) + return bwrq + except Exception as e: + logger.warning(f'Record UID "{record_uid}" encryption error: {e}. Skipping.') + return None + + def _create_breach_watch_password(self, password: str) -> client_pb2.BWPassword: + """Create a breach watch password object.""" + bw_password = client_pb2.BWPassword() + bw_password.value = password + bw_password.resolved = utils.current_milli_time() + bw_password.status = client_pb2.BWStatus.IGNORE + return bw_password + + def _get_existing_breach_watch_euid(self, vault: vault_online.VaultOnline, record_uid: str, password: str) -> Optional[bytes]: + """Get existing breach watch EUID if available.""" + bw_record = vault.vault_data.storage.breach_watch_records.get_entity(record_uid) + if not bw_record: + return None + + try: + record_key = vault.vault_data.get_record_key(record_uid=record_uid) + data = crypto.decrypt_aes_v2(bw_record.data, record_key) + data_obj = json.loads(data.decode()) + except Exception as e: + logger.debug(f'BreachWatch data record "{record_uid}" decrypt error: {e}') + return None + + if data_obj and 'passwords' in data_obj: + existing_password = next((x for x in data_obj['passwords'] if x.get('value', '') == password), None) + if existing_password: + return next((base64.b64decode(x['euid']) for x in data_obj['passwords'] if 'euid' in x), None) + + return None + + def _process_breach_watch_requests(self, vault: vault_online.VaultOnline, bw_requests: list[breachwatch_pb2.BreachWatchRecordRequest]) -> None: + """Process the breach watch requests.""" + # Queue audit event + self._queue_audit_event(vault) + + self._send_breach_watch_requests(vault, bw_requests) + + def _queue_audit_event(self, vault: vault_online.VaultOnline) -> None: + """Queue audit event for the ignore action.""" + audit_plugin = vault.client_audit_event_plugin() + if audit_plugin: + audit_plugin.schedule_audit_event('bw_record_ignored') + + def _send_breach_watch_requests(self, vault: vault_online.VaultOnline, bw_requests: list[breachwatch_pb2.BreachWatchRecordRequest]) -> None: + """Send breach watch requests in chunks.""" + while bw_requests: + chunk = bw_requests[0:999] + bw_requests = bw_requests[999:] + + try: + response = self._send_breach_watch_chunk(vault, chunk) + self._log_breach_watch_response(response) + except Exception as e: + logger.error(f'Error sending breach watch chunk: {e}') + + def _send_breach_watch_chunk(self, vault: vault_online.VaultOnline, chunk: list[breachwatch_pb2.BreachWatchRecordRequest]) -> breachwatch_pb2.BreachWatchUpdateResponse: + """Send a chunk of breach watch requests.""" + rq = breachwatch_pb2.BreachWatchUpdateRequest() + rq.breachWatchRecordRequest.extend(chunk) + + return vault.keeper_auth.execute_auth_rest( + rest_endpoint=UPDATE_BW_RECORD_URL, + request=rq, + response_type=breachwatch_pb2.BreachWatchUpdateResponse + ) + + def _log_breach_watch_response(self, response: breachwatch_pb2.BreachWatchUpdateResponse) -> None: + """Log the breach watch response.""" + for status in response.breachWatchRecordStatus: + record_uid = utils.base64_url_encode(status.recordUid) + logger.info(f'{record_uid}: {status.status} {status.reason}') + +class BreachWatchScanCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='breachwatch scan', description='Scan for breached passwords.' + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--records', '-r', dest='records', type=str, + help='UID of the record to scan') + + def execute(self, context: KeeperParams, **kwargs): + """Main execution method for breach watch scanning.""" + self._validate_context(context) + record_uids = self._get_and_validate_record_uids(kwargs) + for record_uid in record_uids: - logging.warning(f'Record UID "{record_uid}" cannot ignore. Skipping.') + self._scan_single_record(context.vault, record_uid) - if bw_requests: - params.sync_data = True - if params.breach_watch.send_audit_events: - params.queue_audit_event('bw_record_ignored') - - while bw_requests: - chunk = bw_requests[0:999] - bw_requests = bw_requests[999:] - rq = breachwatch_pb2.BreachWatchUpdateRequest() - rq.breachWatchRecordRequest.extend(chunk) - rs = api.communicate_rest(params, rq, 'breachwatch/update_record_data', - rs_type=breachwatch_pb2.BreachWatchUpdateResponse) - for status in rs.breachWatchRecordStatus: - logging.info(f'{utils.base64_url_encode(status.recordUid)}: {status.status} {status.reason}') - """ \ No newline at end of file + def _validate_context(self, context: KeeperParams) -> None: + """Validate that the context has required components.""" + if not context.vault: + raise ValueError("Vault is not initialized.") + + if not context.auth.auth_context.license.get('breachWatchEnabled'): + raise ValueError("Breach watch is not enabled. Please contact your administrator to enable this feature.") + + def _get_and_validate_record_uids(self, kwargs: dict) -> list[str]: + """Extract and validate record UIDs from kwargs.""" + record_uids = kwargs.get('records') + if not record_uids: + raise ValueError("Record UID is required. Use -r or --records to specify the record UID. Example: 'breachwatch scan -r 1234567890'") + + if isinstance(record_uids, str): + record_uids = [record_uids] + + return record_uids + + def _scan_single_record(self, vault: vault_online.VaultOnline, record_uid: str) -> None: + """Scan a single record for breached passwords.""" + # Load the record + record = self._load_record(vault, record_uid) + if not record: + return + + # Extract password + password = self._extract_password(record, record_uid) + if not password: + return + + # Get record key for encryption + record_key = self._get_record_key(vault, record_uid) + if not record_key: + return + + # Perform the breach watch scan + self._perform_breach_watch_scan(vault, record_uid, record_key, password) + + def _load_record(self, vault: vault_online.VaultOnline, record_uid: str): + """Load a record from the vault.""" + record = vault.vault_data.load_record(record_uid) + if not record: + logger.warning(f"Record not found: {record_uid}") + return None + return record + + def _extract_password(self, record: vault_record.PasswordRecord | vault_record.TypedRecord, record_uid: str) -> str: + """Extract password from a record.""" + password = record.extract_password() + if not password: + logger.warning(f"Password not found in record: {record_uid}") + return None + return password + + def _get_record_key(self, vault: vault_online.VaultOnline, record_uid: str): + """Get the record key for encryption/decryption.""" + record_key = vault.vault_data.get_record_key(record_uid) + if not record_key: + logger.warning(f"Record key not found for record: {record_uid}") + return None + return record_key + + def _perform_breach_watch_scan(self, vault: vault_online.VaultOnline, record_uid: str, record_key: bytes, password: str) -> None: + """Perform the actual breach watch scan for a record.""" + try: + bw_password = vault.breach_watch_plugin().scan_and_store_record_status( + record_uid=record_uid, + record_key=record_key, + password=password + ) + + if bw_password: + status = self._get_status_display(bw_password.status) + logger.info(f"Scan completed for record {record_uid}. Status: {status}") + else: + logger.warning(f"Scan failed for record {record_uid}") + + except Exception as e: + logger.error(f"Error scanning record {record_uid}: {str(e)}") + + def _get_status_display(self, status: int) -> str: + return STATUS_TO_TEXT.get(status, "UNKNOWN") \ No newline at end of file From 4c7477d511da05ee26cdedc0e2f902ac4d9e7e39 Mon Sep 17 00:00:00 2001 From: sdubey-ks Date: Thu, 14 Aug 2025 17:57:13 +0530 Subject: [PATCH 21/44] Python SDK Command examples --- .../create_custom_record_type.py | 127 +++++++++ .../custom_record_type_info.py | 102 +++++++ .../delete_custom_record_type.py | 117 ++++++++ .../download_record_types.py | 113 ++++++++ .../edit_custom_record_type.py | 133 +++++++++ .../custom_record_type/load_record_types.py | 115 ++++++++ examples/folder/share_folder.py | 151 ++++++++++ examples/record/add_record.py | 131 +++++++++ examples/record/delete_record.py | 145 ++++++++++ examples/record/list_records.py | 120 ++++++++ examples/record/share_record.py | 150 ++++++++++ examples/record/update_record.py | 262 ++++++++++++++++++ .../create_secrets_manager_app.py | 115 ++++++++ .../get_secrets_manager_app.py | 152 ++++++++++ .../list_secrets_manager_apps.py | 124 +++++++++ .../remove_secrets_manager_app.py | 140 ++++++++++ .../secrets_manager_app_add_record.py | 135 +++++++++ .../secrets_manager_app_remove_record.py | 130 +++++++++ .../secrets_manager_client_add.py | 159 +++++++++++ .../secrets_manager_client_remove.py | 136 +++++++++ .../share_secrets_manager_app.py | 128 +++++++++ .../unshare_secrets_manager_app.py | 121 ++++++++ 22 files changed, 3006 insertions(+) create mode 100644 examples/custom_record_type/create_custom_record_type.py create mode 100644 examples/custom_record_type/custom_record_type_info.py create mode 100644 examples/custom_record_type/delete_custom_record_type.py create mode 100644 examples/custom_record_type/download_record_types.py create mode 100644 examples/custom_record_type/edit_custom_record_type.py create mode 100644 examples/custom_record_type/load_record_types.py create mode 100644 examples/folder/share_folder.py create mode 100644 examples/record/add_record.py create mode 100644 examples/record/delete_record.py create mode 100644 examples/record/list_records.py create mode 100644 examples/record/share_record.py create mode 100644 examples/record/update_record.py create mode 100644 examples/secrets_manager_app/create_secrets_manager_app.py create mode 100644 examples/secrets_manager_app/get_secrets_manager_app.py create mode 100644 examples/secrets_manager_app/list_secrets_manager_apps.py create mode 100644 examples/secrets_manager_app/remove_secrets_manager_app.py create mode 100644 examples/secrets_manager_app/secrets_manager_app_add_record.py create mode 100644 examples/secrets_manager_app/secrets_manager_app_remove_record.py create mode 100644 examples/secrets_manager_app/secrets_manager_client_add.py create mode 100644 examples/secrets_manager_app/secrets_manager_client_remove.py create mode 100644 examples/secrets_manager_app/share_secrets_manager_app.py create mode 100644 examples/secrets_manager_app/unshare_secrets_manager_app.py diff --git a/examples/custom_record_type/create_custom_record_type.py b/examples/custom_record_type/create_custom_record_type.py new file mode 100644 index 00000000..1b3ec754 --- /dev/null +++ b/examples/custom_record_type/create_custom_record_type.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def create_custom_record_type( + vault: vault_online.VaultOnline, + record_type_title: str, + description: Optional[str] = None, + categories: Optional[List[str]] = None, + fields: Optional[List[Dict[str, Any]]] = None +): + """ + Create a new custom record type in the Keeper vault. + + This function creates a custom record type with the specified title, + description, categories, and field definitions. + """ + if description is None: + description = f"Custom record type: {record_type_title}" + + if categories is None: + categories = ["custom", "example"] + + try: + result = record_type_management.create_custom_record_type( + vault, + record_type_title, + fields, + description, + categories=categories + ) + + print(f'Successfully created custom record type: {record_type_title}') + print(f'Description: {description}') + print(f'Categories: {", ".join(categories)}') + print(f'Fields: {", ".join([field.get("$ref", str(field)) for field in fields])}') + print(f'Result: {result}') + + return result + + except Exception as e: + print(f'Error creating custom record type {record_type_title}: {str(e)}') + return None + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Create a custom record type using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python create_custom_record_type.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_type_title = "New Custom Record Type" # Max 32 characters + description = "An example custom record type created by the Keeper SDK" + categories = ["custom", "example"] + field_names = ["login", "password", "url"] + fields = [{"$ref": field} for field in field_names if field] + + try: + vault = login_to_keeper_with_config(args.config).vault + create_custom_record_type(vault, record_type_title, description, categories, fields) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/custom_record_type/custom_record_type_info.py b/examples/custom_record_type/custom_record_type_info.py new file mode 100644 index 00000000..5ba7217a --- /dev/null +++ b/examples/custom_record_type/custom_record_type_info.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_record_type_info(context: KeeperParams, **kwargs): + """ + Execute record type information command. + + This function retrieves and displays information about record types + using the Keeper CLI command infrastructure. + """ + record_type_info_command = RecordTypeInfoCommand() + + try: + record_type_info_command.execute(context=context, **kwargs) + return True + except Exception as e: + print(f'Error: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Get record type information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python custom_record_type_info.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'record_name': '*' + } + + success = execute_record_type_info(context, **kwargs) + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/examples/custom_record_type/delete_custom_record_type.py b/examples/custom_record_type/delete_custom_record_type.py new file mode 100644 index 00000000..50257ee1 --- /dev/null +++ b/examples/custom_record_type/delete_custom_record_type.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def delete_custom_record_type( + vault: vault_online.VaultOnline, + record_type_id: int, + force: bool = False +): + """ + Delete a custom record type from the Keeper vault. + This function removes a custom record type by its ID. + """ + try: + if not force: + response = input(f'Are you sure you want to delete custom record type ID {record_type_id}? (y/N): ') + if response.lower() not in ['y', 'yes']: + print('Deletion cancelled.') + return False + + result = record_type_management.delete_custom_record_types(vault, record_type_id) + + if result: + print(f'Successfully deleted custom record type ID: {record_type_id}') + return True + else: + print(f'Failed to delete custom record type ID: {record_type_id}') + return False + + except Exception as e: + print(f'Error deleting custom record type {record_type_id}: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete a custom record type using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python delete_custom_record_type.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_type_id = 24375 + force = True + + print(f"Note: This example will attempt to delete record type ID {record_type_id}") + + try: + vault = login_to_keeper_with_config(args.config).vault + success = delete_custom_record_type(vault, record_type_id, force) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/custom_record_type/download_record_types.py b/examples/custom_record_type/download_record_types.py new file mode 100644 index 00000000..bf53c315 --- /dev/null +++ b/examples/custom_record_type/download_record_types.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def download_record_types(context: KeeperParams, **kwargs): + """ + Download custom record types to a JSON file. + + This function downloads all custom record types from the vault and saves them + to a JSON file using the CLI command infrastructure. + """ + try: + download_command = DownloadRecordTypesCommand() + + if not kwargs["source"]: + kwargs["source"] = "keeper" + + if not kwargs["name"]: + kwargs["name"] = "record_types.json" + + download_command.execute(context=context, **kwargs) + + print(f'Successfully downloaded record types to: {kwargs["name"]}') + return True + + except Exception as e: + print(f'Error downloading record types: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Download custom record types to a JSON file using Keeper CLI', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python download_record_types.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + try: + context = login_to_keeper_with_config(args.config) + + kwargs = { + 'source': 'keeper', + 'name': 'record_types.json' + } + + success = download_record_types(context, **kwargs) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/custom_record_type/edit_custom_record_type.py b/examples/custom_record_type/edit_custom_record_type.py new file mode 100644 index 00000000..d989f6d2 --- /dev/null +++ b/examples/custom_record_type/edit_custom_record_type.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def edit_custom_record_type( + vault: vault_online.VaultOnline, + record_type_id: int, + title: Optional[str] = None, + fields: Optional[List[Dict[str, Any]]] = None, + description: Optional[str] = None, + categories: Optional[List[str]] = None +): + """ + Edit an existing custom record type in the Keeper vault. + + This function updates a custom record type identified by its ID. You can + update the title, description, categories, and field definitions. Field + definitions should be provided as a list of objects containing a "$ref" + key that points to a valid record field name. + """ + try: + result = record_type_management.edit_custom_record_types( + vault, + record_type_id, + title, + fields, + description, + categories + ) + + print(f'Successfully edited custom record type ID: {record_type_id}') + if title: + print(f'Title: {title}') + if description: + print(f'Description: {description}') + if categories: + print(f'Categories: {", ".join(categories)}') + print(f'Fields: {", ".join([field.get("$ref", str(field)) for field in fields])}') + print(f'Result: {result}') + + return result + + except Exception as e: + print(f'Error editing custom record type {record_type_id}: {str(e)}') + return None + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Edit a custom record type using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python edit_custom_record_type.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_type_id = 24356 + title = "Updated Custom Record New" # Max 32 characters + description = "An example custom record type created by the Keeper SDK" + categories = ["custom", "example"] + field_names = ["login", "password", "url"] + fields = [{"$ref": field} for field in field_names if field] + print(f"Note: This example will attempt to edit record type ID {record_type_id}") + + try: + vault = login_to_keeper_with_config(args.config).vault + edit_custom_record_type(vault, record_type_id, title, fields, description, categories) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/custom_record_type/load_record_types.py b/examples/custom_record_type/load_record_types.py new file mode 100644 index 00000000..6d1433ab --- /dev/null +++ b/examples/custom_record_type/load_record_types.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def load_record_types(context: KeeperParams, **kwargs): + """ + Load custom record types from a JSON file. + + This function loads custom record types from a JSON file and imports them + into the vault using the CLI command infrastructure. + """ + try: + if not os.path.exists(kwargs['file']): + print(f'Input file {kwargs["file"]} not found') + return False + + load_command = LoadRecordTypesCommand() + + load_command.execute(context=context, **kwargs) + + print(f'Successfully loaded record types from: {kwargs["file"]}') + return True + + except Exception as e: + print(f'Error loading record types: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Load custom record types from JSON file using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python load_record_types.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + json_file = 'record_types.json' + + if not os.path.exists(json_file): + print(f'JSON file {json_file} not found') + print("You can create one first using the download_record_types.py example.") + sys.exit(1) + + try: + context = login_to_keeper_with_config(args.config) + kwargs = { + 'file': json_file + } + success = load_record_types(context, **kwargs) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/folder/share_folder.py b/examples/folder/share_folder.py new file mode 100644 index 00000000..270b4eb2 --- /dev/null +++ b/examples/folder/share_folder.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def share_folder_with_user( + context: KeeperParams, + folder_uid: str, + user_email: str, + manage_records: Optional[str] = None, + manage_users: Optional[str] = None, + action: str = 'grant' +): + """ + Share a folder with another user. + This function shares a folder with a user and returns True if successful, False otherwise. + """ + try: + share_command = ShareFolderCommand() + + permissions = [] + if manage_records == 'on': + permissions.append("manage records") + if manage_users == 'on': + permissions.append("manage users") + + permissions_text = f" with {', '.join(permissions)} permissions" if permissions else " (default permissions)" + print(f'Sharing folder "{folder_uid}" with user "{user_email}"{permissions_text}...') + + kwargs = { + 'folder': [folder_uid], + 'user': [user_email], + 'action': action, + 'force': True + } + + if manage_records: + kwargs['manage_records'] = manage_records + if manage_users: + kwargs['manage_users'] = manage_users + + share_command.execute(context=context, **kwargs) + + print(f'Successfully {action}ed folder access for user: {user_email}') + + print('\nShare operation completed successfully') + print('-' * 40) + + context.vault.sync_down() + + print(f'Status: Successfully {action}ed access for {user_email}') + + return True + + except Exception as e: + print(f'Error sharing folder: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Share a folder with another user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python share_folder.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + folder_uid = "t5C4bl3iWmOPWugaWGaMIQ" + user_email = "example@example.com" + manage_records = 'on' + manage_users = 'off' + action = 'grant' + + print(f"Note: This example will attempt to share folder '{folder_uid}' with '{user_email}'") + + try: + context = login_to_keeper_with_config(args.config) + success = share_folder_with_user( + context=context, + folder_uid=folder_uid, + user_email=user_email, + manage_records=manage_records, + manage_users=manage_users, + action=action + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/record/add_record.py b/examples/record/add_record.py new file mode 100644 index 00000000..af24d649 --- /dev/null +++ b/examples/record/add_record.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def add_record( + vault: vault_online.VaultOnline, + title: str, + login: str, + password: str, + url: Optional[str] = None, + notes: Optional[str] = None, + folder_uid: Optional[str] = None +): + """ + Add a new password record to the Keeper vault. + + This function creates a new password record with the specified credentials + and adds it to the vault. The record can optionally be placed in a specific + folder and include additional metadata like URL and notes. + """ + try: + record = vault_record.PasswordRecord() + record.title = title + record.login = login + record.password = password + + if url: + record.link = url + if notes: + record.notes = notes + + result = record_management.add_record_to_folder(vault, record, folder_uid) + + print(f'Successfully added record: {title}') + print(f'Record UID: {result}') + print(f'Login: {login}') + print(f'URL: {url or "N/A"}') + print(f'Notes: {notes or "N/A"}') + if folder_uid: + print(f'Folder UID: {folder_uid}') + + return result + + except Exception as e: + print(f'Error adding record {title}: {str(e)}') + return None + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add a new record to the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python add_record.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + title = "Test Record 1" + login = "example@example.com" + password = "ExamplePassword123!" + url = "https://example.com" + notes = "This is an example record created by the Keeper SDK" + folder_uid = None + + try: + vault = login_to_keeper_with_config(args.config).vault + add_record(vault, title, login, password, url, notes, folder_uid) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/record/delete_record.py b/examples/record/delete_record.py new file mode 100644 index 00000000..c7f905e0 --- /dev/null +++ b/examples/record/delete_record.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def delete_record(vault: vault_online.VaultOnline, record_uid: str, force: bool = False): + """ + Delete a record from the Keeper vault. + + This function locates a record by its UID and removes it from the vault. + If force is False, it will prompt for confirmation before deletion. + """ + try: + record = vault.vault_data.get_record(record_uid) + if not record: + print(f"Record with UID '{record_uid}' not found.") + return False + + print(f"Found record: '{record.title}'") + + record_path = vault_types.RecordPath(record_uid=record_uid, folder_uid='') + + def confirm_deletion(summary: str) -> bool: + if force: + return True + print("Deletion Summary:") + print(summary) + response = input("Proceed with deletion? (y/n): ").lower() + return response in ['y', 'yes'] + + record_management.delete_vault_objects(vault, [record_path], confirm=confirm_deletion) + print(f'Successfully deleted record: {record.title} ({record_uid})') + return True + + except Exception as e: + print(f'Error deleting record {record_uid}: {str(e)}') + return False + + +def find_record_by_title(vault: vault_online.VaultOnline, title: str) -> Optional[str]: + """ + Find a record's UID by searching for its title. + + Performs a case-insensitive search through all records in the vault + to find one with a matching title. + """ + try: + for record in vault.vault_data.records(): + if record.title.lower() == title.lower(): + return record.record_uid + return None + except Exception as e: + print(f'Error searching for record: {str(e)}') + return None + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete a record from the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python delete_record.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + title_to_delete = "Test Record 1" + force_delete = True + + try: + vault = login_to_keeper_with_config(args.config).vault + + record_uid = find_record_by_title(vault, title_to_delete) + if not record_uid: + print(f"No record found with title: '{title_to_delete}'") + print(f"Note: This example looks for a record titled '{title_to_delete}'") + print("You can create one first using the add_record.py example.") + sys.exit(1) + + success = delete_record(vault, record_uid, force_delete) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/record/list_records.py b/examples/record/list_records.py new file mode 100644 index 00000000..ae817397 --- /dev/null +++ b/examples/record/list_records.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def list_records( + context: KeeperParams, + show_details: bool = False, + criteria: Optional[str] = None, + record_type: Optional[str] = None +): + """ + List and display records from the Keeper vault with optional filtering. + + This function uses the Keeper CLI `RecordListCommand` to retrieve and display + records based on the provided criteria and filters. + """ + try: + list_command = RecordListCommand() + + kwargs = { + 'verbose': show_details, + 'format': 'table', + 'search_text': criteria, + } + if record_type: + kwargs['record_type'] = record_type + + list_command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error listing records: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List all records in the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python list_records.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + show_details = True + criteria = None + record_type = None + + try: + context = login_to_keeper_with_config(args.config) + list_records( + context, + show_details=show_details, + criteria=criteria, + record_type=record_type, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/record/share_record.py b/examples/record/share_record.py new file mode 100644 index 00000000..0e325b2f --- /dev/null +++ b/examples/record/share_record.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def share_record_with_user( + context: KeeperParams, + record_uid: str, + user_email: str, + can_edit: bool = False, + can_share: bool = False, + action: str = 'grant' +): + """ + Share a record with another user. + + This function shares a vault record with another user using the CLI + command infrastructure. It allows configuring edit and share permissions + for the recipient. + """ + try: + share_command = ShareRecordCommand() + + permissions = [] + if can_edit: + permissions.append("edit") + if can_share: + permissions.append("re-share") + + permissions_text = f" with {', '.join(permissions)} permissions" if permissions else " (read-only)" + print(f'Sharing record "{record_uid}" with user "{user_email}"{permissions_text}...') + + kwargs = { + 'record': record_uid, + 'email': [user_email], + 'action': action, + 'can_edit': can_edit, + 'can_share': can_share, + 'force': True + } + + share_command.execute(context=context, **kwargs) + + print(f'Successfully {action}ed record access for user: {user_email}') + + print('\nShare operation completed successfully') + print('-' * 40) + + context.vault.sync_down() + + print(f'Status: Successfully {action}ed access for {user_email}') + + return True + + except Exception as e: + print(f'Error sharing record: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Share a record with another user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python share_record.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_uid = "UkezdUGQoTOztfi5cGFJnQ" + user_email = "example@example.com" + can_edit = True + can_share = False + action = 'grant' + + print(f"Note: This example will attempt to share record '{record_uid}' with '{user_email}'") + + try: + context = login_to_keeper_with_config(args.config) + success = share_record_with_user( + context=context, + record_uid=record_uid, + user_email=user_email, + can_edit=can_edit, + can_share=can_share, + action=action + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/record/update_record.py b/examples/record/update_record.py new file mode 100644 index 00000000..d537a200 --- /dev/null +++ b/examples/record/update_record.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def find_record_by_criteria( + vault: vault_online.VaultOnline, + criteria: str, + record_type: Optional[str] = None, + record_version: Optional[int] = None +): + """ + Find a record by various criteria including UID, title, or search terms. + + This function searches for records using multiple methods: + 1. Direct UID lookup + 2. Search by criteria with optional type/version filters + 3. Interactive selection if multiple matches + """ + record_info = vault.vault_data.get_record(criteria) + if record_info: + return record_info + + records = list(vault.vault_data.find_records(criteria=criteria, record_type=record_type, record_version=record_version)) + + if not records: + return None + elif len(records) == 1: + return records[0] + else: + print(f'Found {len(records)} records matching "{criteria}":') + for i, record in enumerate(records, 1): + print(f'{i}. {record.title} ({record.record_uid})') + + while True: + try: + choice = int(input('Select a record (number): ')) - 1 + if 0 <= choice < len(records): + return records[choice] + else: + print('Invalid choice. Please try again.') + except ValueError: + print('Please enter a valid number.') + + +def update_password_record( + vault: vault_online.VaultOnline, + record_uid: str, + updates: Dict[str, str] +): + """ + Update a password record with new field values. + + This function loads a password record and updates its fields with the + provided values. It validates that the record is indeed a password record + and provides detailed feedback about the changes being made. + """ + record = vault.vault_data.load_record(record_uid) + if not isinstance(record, vault_record.PasswordRecord): + raise ValueError(f'Record {record_uid} is not a password record') + + print(f'Updating password record: {record.title}') + + if 'title' in updates: + print(f' Title: "{record.title}" -> "{updates["title"]}"') + record.title = updates['title'] + + if 'login' in updates: + print(f' Username: "{record.login or ""}" -> "{updates["login"]}"') + record.login = updates['login'] + + if 'password' in updates: + print(f' Password: {"*" * len(record.password or "")} -> {"*" * len(updates["password"])}') + record.password = updates['password'] + + if 'link' in updates: + print(f' URL: "{record.link or ""}" -> "{updates["link"]}"') + record.link = updates['link'] + + if 'notes' in updates: + notes_preview = (record.notes[:50] + '...') if record.notes and len(record.notes) > 50 else (record.notes or '') + new_notes_preview = (updates['notes'][:50] + '...') if len(updates['notes']) > 50 else updates['notes'] + print(f' Notes: "{notes_preview}" -> "{new_notes_preview}"') + record.notes = updates['notes'] + + return record + + +def update_typed_record( + vault: vault_online.VaultOnline, + record_uid: str, + updates: Dict[str, Any] +): + """ + Update a typed record with new field values. + + This function loads a typed record (v3 record format) and updates its + fields with the provided values. It handles the complex field structure + of typed records and provides detailed feedback about changes. + """ + record = vault.vault_data.load_record(record_uid) + if not isinstance(record, vault_record.TypedRecord): + raise ValueError(f'Record {record_uid} is not a typed record') + + print(f'Updating typed record: {record.title} (type: {record.record_type})') + + if 'title' in updates: + print(f' Title: "{record.title}" -> "{updates["title"]}"') + record.title = updates['title'] + + if 'notes' in updates: + notes_preview = (record.notes[:50] + '...') if record.notes and len(record.notes) > 50 else (record.notes or '') + new_notes_preview = (updates['notes'][:50] + '...') if len(updates['notes']) > 50 else updates['notes'] + print(f' Notes: "{notes_preview}" -> "{new_notes_preview}"') + record.notes = updates['notes'] + + for field_name, field_value in updates.items(): + if field_name in ('title', 'notes'): + continue + + field_found = False + for field in record.fields: + if field.type == field_name: + if hasattr(field, 'value') and field.value: + old_value = field.value[0] if isinstance(field.value, list) and field.value else field.value + print(f' {field_name}: "{old_value}" -> "{field_value}"') + field.value = [field_value] if isinstance(field.value, list) else field_value + field_found = True + break + + if not field_found: + print(f' Warning: Field "{field_name}" not found in record') + + return record + + +def update_record(vault: vault_online.VaultOnline, record_criteria: str, updates: Dict[str, Any], record_type: Optional[str] = None, record_version: Optional[int] = None): + """ + Update an existing record in the vault. + + This function finds a record by criteria, updates its fields, and saves the changes. + It supports both password and typed record formats. + """ + try: + record_info = find_record_by_criteria(vault, record_criteria, record_type=record_type, record_version=record_version) + if not record_info: + print(f'No record found matching "{record_criteria}"') + return False + + print(f'Found record: {record_info.title} ({record_info.record_uid})') + + if record_info.version == 2: + updated_record = update_password_record(vault, record_info.record_uid, updates) + elif record_info.version == 3: + updated_record = update_typed_record(vault, record_info.record_uid, updates) + else: + raise ValueError(f'Unsupported record version: {record_info.version}') + + print('\nSaving changes...') + record_management.update_record(vault, updated_record) + + vault.sync_down() + + print(f'Successfully updated record: {updated_record.title}') + return True + + except Exception as e: + print(f'Error updating record: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Update an existing record in the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python update_record.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_to_update_uid = "UkezdUGQoTOztfi5cGFJnQ" + record_type = None + record_version = None + updates = { + 'title': 'Updated Example Record', + 'login': 'updated@example.com', + 'password': 'UpdatedPassword123!', + 'link': 'https://updated-example.com', + 'notes': 'This record has been updated by the Keeper SDK example' + } + + try: + vault = login_to_keeper_with_config(args.config).vault + success = update_record(vault, record_to_update_uid, updates, record_type=record_type, record_version=record_version) + + if success: + print('\nRecord update completed successfully!') + else: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/create_secrets_manager_app.py b/examples/secrets_manager_app/create_secrets_manager_app.py new file mode 100644 index 00000000..065dd4bb --- /dev/null +++ b/examples/secrets_manager_app/create_secrets_manager_app.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def create_secrets_manager_app( + vault: vault_online.VaultOnline, + app_name: str, + force_add: bool = False +): + """ + Create a new Secrets Manager application in the Keeper vault. + + This function creates a new Secrets Manager application that can be used + to programmatically access vault records through the Secrets Manager API. + The application will be configured with appropriate permissions and credentials. + """ + try: + result = ksm_management.create_secrets_manager_app( + vault=vault, + name=app_name, + force_add=force_add + ) + + if result: + print(f'Successfully created Secrets Manager application: {app_name}, UID: {result}') + return result + else: + print(f'Failed to create Secrets Manager application: {app_name}') + return None + + except Exception as e: + print(f'Error creating Secrets Manager application {app_name}: {str(e)}') + return None + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Create a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python create_secrets_manager_app.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_name = "Secrets Manager App 1" + force = True + + try: + vault = login_to_keeper_with_config(args.config).vault + result = create_secrets_manager_app(vault, app_name, force) + + if result is None: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/get_secrets_manager_app.py b/examples/secrets_manager_app/get_secrets_manager_app.py new file mode 100644 index 00000000..0fae6c21 --- /dev/null +++ b/examples/secrets_manager_app/get_secrets_manager_app.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def print_client_device_info(client_devices): + """Print client device information in a table-like format.""" + for index, client_device in enumerate(client_devices, start=1): + client_devices_str = f"\nClient Device {index}\n" \ + f"=============================\n" \ + f' Device Name: {client_device.name}\n' \ + f' Short ID: {client_device.short_id}\n' \ + f' Created On: {client_device.created_on}\n' \ + f' Expires On: {client_device.expires_on}\n' \ + f' First Access: {client_device.first_access}\n' \ + f' Last Access: {client_device.last_access}\n' \ + f' IP Lock: {client_device.ip_lock}\n' \ + f' IP Address: {client_device.ip_address or "--"}' + logger.info(client_devices_str) + +def print_shared_secrets_info(shared_secrets): + """Print shared secrets information in a table-like format.""" + if not shared_secrets: + return + + # Print table header + logger.info(f"\n{'Share Type':<15} {'UID':<25} {'Title':<30} {'Permissions'}") + logger.info("-" * 85) + + # Print each shared secret + for secrets in shared_secrets: + share_type = str(secrets.type)[:14] + uid = str(secrets.uid)[:24] + name = str(secrets.name)[:29] + permissions = str(secrets.permissions) + logger.info(f"{share_type:<15} {uid:<25} {name:<30} {permissions}") + +def get_secrets_manager_app(vault: vault_online.VaultOnline, app_id: str): + """Retrieve and display Secrets Manager application details by UID or title.""" + try: + app = ksm_management.get_secrets_manager_app(vault, app_id) + + if not app: + logger.info(f'No Secrets Manager application found with ID: {app_id}') + return None + + # Use the same format as keepercli secrets_manager.py + logger.info(f'\nSecrets Manager Application\n' + f'App Name: {app.name}\n' + f'App UID: {app.uid}') + + if app.client_devices and len(app.client_devices) > 0: + print_client_device_info(app.client_devices) + else: + logger.info('\nNo client devices registered for this Application\n') + + if app.shared_secrets: + print_shared_secrets_info(app.shared_secrets) + else: + logger.info('\tThere are no shared secrets to this application') + + return app + + except Exception as e: + logger.error(f'Error getting Secrets Manager application {app_id}: {str(e)}') + return None + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Get details of a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python get_secrets_manager_app.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "7SkD_s3S9AghvrRP8D0gPQ" + + logger.info(f"Note: This example will attempt to get details for app ID '{app_id}'") + + try: + vault = login_to_keeper_with_config(args.config).vault + app_details = get_secrets_manager_app(vault, app_id) + + if app_details is None: + sys.exit(1) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/list_secrets_manager_apps.py b/examples/secrets_manager_app/list_secrets_manager_apps.py new file mode 100644 index 00000000..8330457a --- /dev/null +++ b/examples/secrets_manager_app/list_secrets_manager_apps.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def print_apps_table(apps): + """Print applications in a table-like format with key attributes.""" + if not apps: + logger.info('No Secrets Manager applications found.') + return + + logger.info(f"\n{'App name':<20} {'App UID':<25} {'Records':<8} {'Folders':<8} {'Devices':<8} {'Last Access'}") + logger.info("-" * 95) + + for app in apps: + app_name = str(app.name)[:19] if hasattr(app, 'name') else 'Unknown' + app_uid = str(app.uid)[:24] if hasattr(app, 'uid') else 'Unknown' + records = str(app.records) if hasattr(app, 'records') else '0' + folders = str(app.folders) if hasattr(app, 'folders') else '0' + devices = str(app.count) if hasattr(app, 'count') else '0' + last_access = str(app.last_access) if hasattr(app, 'last_access') else 'Never' + + logger.info(f"{app_name:<20} {app_uid:<25} {records:<8} {folders:<8} {devices:<8} {last_access}") + +def list_secrets_manager_apps(vault: vault_online.VaultOnline): + """ + List all Secrets Manager applications in the Keeper vault. + + This function retrieves and displays all Secrets Manager applications + associated with the current vault. + """ + try: + apps = ksm_management.list_secrets_manager_apps(vault) + + if not apps: + logger.info('No Secrets Manager applications found.') + return None + + print_apps_table(apps) + + return apps + + except Exception as e: + logger.error(f'Error listing Secrets Manager applications: {str(e)}') + return None + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List Secrets Manager applications using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python list_secrets_manager_apps.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + try: + vault = login_to_keeper_with_config(args.config).vault + list_secrets_manager_apps(vault) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/remove_secrets_manager_app.py b/examples/secrets_manager_app/remove_secrets_manager_app.py new file mode 100644 index 00000000..5b8f1178 --- /dev/null +++ b/examples/secrets_manager_app/remove_secrets_manager_app.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def remove_secrets_manager_app( + vault: vault_online.VaultOnline, + uid_or_name: str, + force: bool = False +): + """ + Remove a Secrets Manager application by UID or name. + + This function removes a Secrets Manager application by its UID or name. + """ + try: + try: + app_details = ksm_management.get_secrets_manager_app(vault, uid_or_name) + print(f'Found Secrets Manager Application:') + print('=' * 50) + print(f'Name: {app_details.name}') + print(f'UID: {app_details.uid}') + print(f'Records: {app_details.records}') + print(f'Folders: {app_details.folders}') + print(f'Client Count: {app_details.count}') + print('=' * 50) + + if (app_details.records > 0 or app_details.folders > 0 or app_details.count > 0) and not force: + print('WARNING: This application has:') + if app_details.records > 0: + print(f' - {app_details.records} shared record(s)') + if app_details.folders > 0: + print(f' - {app_details.folders} shared folder(s)') + if app_details.count > 0: + print(f' - {app_details.count} client device(s)') + print('Use --force flag to remove the application anyway.') + return None + + except Exception as e: + print(f'Warning: Could not retrieve app details: {str(e)}') + print('Proceeding with removal attempt...') + + removed_uid = ksm_management.remove_secrets_manager_app(vault, uid_or_name, force=force) + + print(f'Successfully removed Secrets Manager application: {removed_uid}') + return removed_uid + + except ValueError as e: + if 'Cannot remove application with clients' in str(e): + print(f'Error: {str(e)}') + print('Use --force flag to remove the application anyway.') + else: + print(f'Error: {str(e)}') + return None + except Exception as e: + print(f'Error removing Secrets Manager application {uid_or_name}: {str(e)}') + return None + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Remove a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python remove_secrets_manager_app.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + uid_or_name = "Secrets Manager App 1" + force = True + + print(f"Note: This example will attempt to remove app '{uid_or_name}'") + + try: + vault = login_to_keeper_with_config(args.config).vault + removed_app = remove_secrets_manager_app(vault, uid_or_name, force) + + if removed_app is None: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_app_add_record.py b/examples/secrets_manager_app/secrets_manager_app_add_record.py new file mode 100644 index 00000000..566d9060 --- /dev/null +++ b/examples/secrets_manager_app/secrets_manager_app_add_record.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def add_secrets_to_app( + context: KeeperParams, + app_id: str, + secret_uids: List[str], + is_editable: bool = False +): + """ + Add secrets (records) to a Secrets Manager application. + + This function adds one or more secrets to an existing Secrets Manager application, + allowing the application to access those secrets through the Secrets Manager API. + The secrets can be configured as read-only or editable. + """ + try: + print(f'Adding secrets to application "{app_id}"...') + + sm_share_command = SecretsManagerShareCommand() + + editable_text = " (editable)" if is_editable else " (read-only)" + secret_list = ', '.join(secret_uids) + print(f'Adding secrets to application "{app_id}"{editable_text}...') + print(f'Secret UIDs: {secret_list}') + + kwargs = { + 'command': 'add', + 'app': app_id, + 'secret': ' '.join(secret_uids), + 'editable': is_editable + } + + sm_share_command.execute(context=context, **kwargs) + + print(f'Successfully added {len(secret_uids)} secret(s) to application: {app_id}') + + context.vault.sync_down() + return True + + except Exception as e: + print(f'Error adding secrets to Secrets Manager application: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add secrets to a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python secrets_manager_share_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "RlO6y-idGBqu1Ax2yUYXKw" + secret_uids = ["YJAAssUpHCf-2Xfjnlw5cw"] + is_editable = False + + print(f"Note: This example will attempt to add secrets to app ID '{app_id}'") + + try: + context = login_to_keeper_with_config(args.config) + success = add_secrets_to_app( + context=context, + app_id=app_id, + secret_uids=secret_uids, + is_editable=is_editable + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_app_remove_record.py b/examples/secrets_manager_app/secrets_manager_app_remove_record.py new file mode 100644 index 00000000..e8611b19 --- /dev/null +++ b/examples/secrets_manager_app/secrets_manager_app_remove_record.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def remove_secrets_from_app( + context: KeeperParams, + app_id: str, + secret_uids: List[str] +): + """ + Remove secrets (records) from a Secrets Manager application. + + This function removes one or more secrets from an existing Secrets Manager application, + revoking the application's access to those secrets through the Secrets Manager API. + """ + try: + print(f'Removing secrets from application "{app_id}"...') + + sm_share_command = SecretsManagerShareCommand() + + secret_list = ', '.join(secret_uids) + print(f'Removing secrets from application "{app_id}"...') + print(f'Secret UIDs: {secret_list}') + + kwargs = { + 'command': 'remove', + 'app': app_id, + 'secret': ' '.join(secret_uids) + } + + sm_share_command.execute(context=context, **kwargs) + + print(f'Successfully removed {len(secret_uids)} secret(s) from application: {app_id}') + + context.vault.sync_down() + return True + + except Exception as e: + print(f'Error removing secrets from Secrets Manager application: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Remove secrets from a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python secrets_manager_share_remove.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "RlO6y-idGBqu1Ax2yUYXKw" + secret_uids = ["YJAAssUpHCf-2Xfjnlw5cw"] + + print(f"Note: This example will attempt to remove secrets from app ID '{app_id}'") + + try: + context = login_to_keeper_with_config(args.config) + success = remove_secrets_from_app( + context=context, + app_id=app_id, + secret_uids=secret_uids + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_client_add.py b/examples/secrets_manager_app/secrets_manager_client_add.py new file mode 100644 index 00000000..b3fc3aea --- /dev/null +++ b/examples/secrets_manager_app/secrets_manager_client_add.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def add_client_to_app( + context: KeeperParams, + app_id: str, + client_name: str, + count: int = 1, + unlock_ip: bool = False, + first_access_expires_in: int = 60, + access_expire_in_min: Optional[int] = None, + return_tokens: bool = False +): + """ + Add a client to a Secrets Manager application. + + This function adds one or more clients to an existing Secrets Manager application, + allowing the clients to access secrets through the Secrets Manager API. The function + provides comprehensive configuration options for client access control. + """ + try: + client_command = SecretsManagerClientCommand() + + print(f'Adding {count} client(s) to application "{app_id}"...') + if client_name: + print(f'Client name: {client_name}') + if unlock_ip: + print('- IP address unlocked') + else: + print('- IP address locked (default)') + print(f'- First access expires in: {first_access_expires_in} minutes') + if access_expire_in_min: + print(f'- Access expires in: {access_expire_in_min} minutes') + + kwargs = { + 'command': 'add', + 'app': app_id, + 'name': client_name, + 'count': count, + 'unlockIp': unlock_ip, + 'firstAccessExpiresIn': first_access_expires_in, + 'accessExpireInMin': access_expire_in_min, + 'returnTokens': return_tokens + } + + result = client_command.execute(context=context, **kwargs) + + print('=' * 60) + print(f'Successfully added {count} client(s) to application: {app_id}') + + if return_tokens and result: + print(f'Generated tokens: {result}') + + return True + + except Exception as e: + print(f'Error adding client to Secrets Manager application: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add a client to a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python secrets_manager_client_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "RlO6y-idGBqu1Ax2yUYXKw" + client_name = "DemoClient" + count = 1 + unlock_ip = False + first_access_expires_in = 60 + access_expire_in_min = None + return_tokens = True + + print(f"Note: This example will attempt to add a client to app ID '{app_id}'") + + try: + context = login_to_keeper_with_config(args.config) + success = add_client_to_app( + context=context, + app_id=app_id, + client_name=client_name, + count=count, + unlock_ip=unlock_ip, + first_access_expires_in=first_access_expires_in, + access_expire_in_min=access_expire_in_min, + return_tokens=return_tokens + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_client_remove.py b/examples/secrets_manager_app/secrets_manager_client_remove.py new file mode 100644 index 00000000..41e466b4 --- /dev/null +++ b/examples/secrets_manager_app/secrets_manager_client_remove.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + + +def remove_client_from_app( + context: KeeperParams, + app_id: str, + client_names_or_ids: List[str], + force: bool = False +): + """ + Remove client(s) from a Secrets Manager application. + + This function removes one or more clients from a Secrets Manager application + using the CLI command infrastructure. It provides confirmation prompts + unless force is True. + """ + try: + client_command = SecretsManagerClientCommand() + + if len(client_names_or_ids) == 1 and client_names_or_ids[0] in ['*', 'all']: + print(f'Removing ALL clients from application "{app_id}"...') + else: + clients_text = ', '.join(client_names_or_ids) + print(f'Removing client(s) from application "{app_id}": {clients_text}') + + if force: + print('- Force mode: Skipping confirmation prompts') + + kwargs = { + 'command': 'remove', + 'app': app_id, + 'client_names_or_ids': client_names_or_ids, + 'force': force + } + + client_command.execute(context=context, **kwargs) + print(f'Successfully removed client(s) from application: {app_id}') + context.vault.sync_down() + return True + + except Exception as e: + print(f'Error removing client from Secrets Manager application: {str(e)}') + return False +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Remove a client from a Secrets Manager application using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python secrets_manager_client_remove.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "RlO6y-idGBqu1Ax2yUYXKw" + client_names_or_ids = ["DemoClient"] + force = True + + print(f"Note: This example will attempt to remove clients from app ID '{app_id}'") + + try: + context = login_to_keeper_with_config(args.config) + success = remove_client_from_app( + context=context, + app_id=app_id, + client_names_or_ids=client_names_or_ids, + force=force + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/share_secrets_manager_app.py b/examples/secrets_manager_app/share_secrets_manager_app.py new file mode 100644 index 00000000..efafb990 --- /dev/null +++ b/examples/secrets_manager_app/share_secrets_manager_app.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def share_secrets_manager_app( + context: KeeperParams, + app_id: str, + user_email: str, + is_admin: bool = False +): + """ + Share a Secrets Manager application with a user; optionally grant admin permissions. + + This function shares a Secrets Manager application with another user. It first retrieves + the application details, then shares it with the specified user. If the user is granted + admin permissions, the application will be shared with full access. + """ + try: + sm_app_command = SecretsManagerAppCommand() + kwargs = { + 'command': 'share', + 'app': app_id, + 'email': user_email + } + + if is_admin: + kwargs['admin'] = True + + sm_app_command.execute(context=context, **kwargs) + + print(f'Successfully shared with user: {user_email}') + context.vault.sync_down() + + return True + + except Exception as e: + print(f'Error sharing Secrets Manager application: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Share a Secrets Manager application with another user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python share_secrets_manager_app.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "RlO6y-idGBqu1Ax2yUYXKw" + user_email = "example@example.com" + is_admin = False + + print(f"Note: This example will attempt to share app ID '{app_id}'") + + try: + context = login_to_keeper_with_config(args.config) + success = share_secrets_manager_app( + context=context, + app_id=app_id, + user_email=user_email, + is_admin=is_admin + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file diff --git a/examples/secrets_manager_app/unshare_secrets_manager_app.py b/examples/secrets_manager_app/unshare_secrets_manager_app.py new file mode 100644 index 00000000..f74e241c --- /dev/null +++ b/examples/secrets_manager_app/unshare_secrets_manager_app.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def unshare_secrets_manager_app( + context: KeeperParams, + app_id: str, + user_email: str +): + """Unshare a Secrets Manager application from a specific user. + + This function unshares a Secrets Manager application from a specific user. It first retrieves + the application details, then unshares it from the specified user. + """ + try: + sm_app_command = SecretsManagerAppCommand() + kwargs = { + 'command': 'unshare', + 'app': app_id, + 'email': user_email + } + + sm_app_command.execute(context=context, **kwargs) + + print(f'Successfully unshared from user: {user_email}') + + return True + + except Exception as e: + print(f'Error unsharing Secrets Manager application: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Unshare a Secrets Manager application from a user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python unshare_secrets_manager_app.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + app_id = "RlO6y-idGBqu1Ax2yUYXKw" + user_email = "example@example.com" + + print(f"Note: This example will attempt to unshare app ID '{app_id}'") + + try: + context = login_to_keeper_with_config(args.config) + success = unshare_secrets_manager_app( + context=context, + app_id=app_id, + user_email=user_email + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file From d0ad7a713ccc991b466e6550009565dc23ebe0c5 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 20 Aug 2025 13:12:50 +0530 Subject: [PATCH 22/44] Protobuff file updates --- .../src/keepercli/commands/record_edit.py | 59 +- .../src/keepercli/commands/secrets_manager.py | 6 +- .../src/keepercli/helpers/ksm_utils.py | 6 +- .../src/keepercli/helpers/record_utils.py | 4 +- .../src/keepersdk/proto/APIRequest_pb2.py | 780 +++++++++--------- .../src/keepersdk/proto/APIRequest_pb2.pyi | 167 +++- .../src/keepersdk/proto/AccountSummary_pb2.py | 80 +- .../keepersdk/proto/AccountSummary_pb2.pyi | 22 +- .../src/keepersdk/proto/BI_pb2.py | 293 ++++--- .../src/keepersdk/proto/BI_pb2.pyi | 262 ++++++ .../src/keepersdk/proto/GraphSync_pb2.py | 24 +- .../src/keepersdk/proto/GraphSync_pb2.pyi | 78 +- .../keepersdk/proto/NotificationCenter_pb2.py | 74 +- .../proto/NotificationCenter_pb2.pyi | 24 +- .../src/keepersdk/proto/SyncDown_pb2.py | 24 +- .../src/keepersdk/proto/SyncDown_pb2.pyi | 6 + .../src/keepersdk/proto/automator_pb2.py | 13 +- .../src/keepersdk/proto/breachwatch_pb2.py | 12 +- .../src/keepersdk/proto/client_pb2.py | 12 +- .../src/keepersdk/proto/enterprise_pb2.py | 600 +++++++------- .../src/keepersdk/proto/enterprise_pb2.pyi | 6 +- .../src/keepersdk/proto/folder_pb2.py | 12 +- .../src/keepersdk/proto/pam_pb2.py | 44 +- .../src/keepersdk/proto/pam_pb2.pyi | 34 +- .../src/keepersdk/proto/pedm_pb2.py | 106 ++- .../src/keepersdk/proto/pedm_pb2.pyi | 28 +- .../src/keepersdk/proto/push_pb2.py | 12 +- .../src/keepersdk/proto/record_pb2.py | 12 +- .../src/keepersdk/proto/router_pb2.py | 116 ++- .../src/keepersdk/proto/router_pb2.pyi | 16 +- .../src/keepersdk/proto/ssocloud_pb2.py | 12 +- .../src/keepersdk/proto/version_pb2.py | 12 +- 32 files changed, 1681 insertions(+), 1275 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index 60064df6..c87bc5f7 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -1209,7 +1209,7 @@ def _display_record(self, vault: vault_data.VaultData, record, output_format: st """Display a record in the specified format.""" record_uid = record.record_uid dispatch = { - 'json': lambda: self._display_record_json(vault, record_uid), + 'json': lambda: self._display_record_json(vault, record_uid, unmask), 'password': lambda: self._display_record_password(vault, record_uid), 'fields': lambda: self._display_record_fields(vault, record_uid, unmask) } @@ -1238,18 +1238,18 @@ def _display_team(self, vault: vault_data.VaultData, team, output_format: str): else: # detail format self._display_team_detail(vault, team.team_uid) - def _display_record_json(self, vault: vault_data.VaultData, uid: str): + def _display_record_json(self, vault: vault_data.VaultData, uid: str, unmask: bool = False): """Display record information in JSON format.""" record = vault.vault_data.get_record(record_uid=uid) record_data = vault.vault_data.load_record(record_uid=uid) - output = self._build_record_json_output(record, record_data, uid) + output = self._build_record_json_output(record, record_data, uid, unmask) self._add_share_info_to_json(vault, uid, output) logger.info(json.dumps(output, indent=2)) - def _build_record_json_output(self, record, record_data, uid: str): + def _build_record_json_output(self, record, record_data, uid: str, unmask: bool = False): """Build the JSON output structure for a record.""" output = { 'Record UID:': uid, @@ -1258,29 +1258,29 @@ def _build_record_json_output(self, record, record_data, uid: str): } if isinstance(record_data, vault_record.PasswordRecord): - self._add_password_record_json_fields(record_data, output) + self._add_password_record_json_fields(record_data, output, unmask) elif isinstance(record_data, vault_record.TypedRecord): - self._add_typed_record_json_fields(record_data, output) + self._add_typed_record_json_fields(record_data, output, unmask) elif isinstance(record_data, vault_record.FileRecord): self._add_file_record_json_fields(record_data, output) else: raise ValueError('Record data could not be displayed. Record is of unsupported type for this command(eg Application record)') - output['Last Modified:'] = record_data.client_time_modified + output['Last Modified:'] = datetime.datetime.fromtimestamp(record_data.client_time_modified / 1000).strftime('%Y-%m-%d %H:%M:%S') if record_data.client_time_modified else None output['Version:'] = record.version output['Revision'] = record.revision return output - def _add_password_record_json_fields(self, record_data: vault_record.PasswordRecord, output: dict): + def _add_password_record_json_fields(self, record_data: vault_record.PasswordRecord, output: dict, unmask: bool = False): """Add password record specific fields to JSON output.""" output['Notes:'] = record_data.notes output['$login:'] = record_data.login - output['$password:'] = record_data.password + output['$password:'] = '********' if not unmask else record_data.password output['$link:'] = record_data.link if record_data.totp: - output['Totp:'] = record_data.totp + output['Totp:'] = '********' if not unmask else record_data.totp if record_data.attachments: output['Attachments:'] = [{ @@ -1290,13 +1290,42 @@ def _add_password_record_json_fields(self, record_data: vault_record.PasswordRec } for a in record_data.attachments] if record_data.custom: - output['Custom fields:'] = [vault_extensions.extract_typed_field(field) for field in record_data.custom] - - def _add_typed_record_json_fields(self, record_data: vault_record.TypedRecord, output: dict): + custom_output = [] + for field in record_data.custom: + field_data = vault_extensions.extract_typed_field(field) + if not unmask and self._is_sensitive_field_type(field.type): + if isinstance(field_data, dict) and 'value' in field_data: + field_data['value'] = '********' + elif isinstance(field_data, str): + field_data = '********' + custom_output.append(field_data) + output['Custom fields:'] = custom_output + + def _add_typed_record_json_fields(self, record_data: vault_record.TypedRecord, output: dict, unmask: bool = False): """Add typed record specific fields to JSON output.""" output['Notes:'] = record_data.notes - output['Fields:'] = [vault_extensions.extract_typed_field(field) for field in record_data.fields] - output['Custom:'] = [vault_extensions.extract_typed_field(field) for field in record_data.custom] + + fields_output = [] + for field in record_data.fields: + field_data = vault_extensions.extract_typed_field(field) + if not unmask and self._is_sensitive_field_type(field.type): + if isinstance(field_data, dict) and 'value' in field_data: + field_data['value'] = '********' + elif isinstance(field_data, str): + field_data = '********' + fields_output.append(field_data) + output['Fields:'] = fields_output + + custom_output = [] + for field in record_data.custom: + field_data = vault_extensions.extract_typed_field(field) + if not unmask and self._is_sensitive_field_type(field.type): + if isinstance(field_data, dict) and 'value' in field_data: + field_data['value'] = '********' + elif isinstance(field_data, str): + field_data = '********' + custom_output.append(field_data) + output['Custom:'] = custom_output def _add_file_record_json_fields(self, record_data: vault_record.FileRecord, output: dict): """Add file record specific fields to JSON output.""" diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index b15c1406..b5db885e 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -11,6 +11,7 @@ from keepersdk.proto.APIRequest_pb2 import AddAppClientRequest, Device, RemoveAppClientsRequest, AppShareAdd, ApplicationShareType, AddAppSharesRequest, RemoveAppSharesRequest from keepersdk.proto.enterprise_pb2 import GENERAL from keepersdk.vault import ksm_management, vault_online +from keepersdk.vault.vault_record import TypedRecord from . import base from .share_management import ShareAction, ShareFolderCommand, ShareRecordCommand @@ -916,6 +917,9 @@ def _process_secret(vault: vault_online.VaultOnline, secret_uid: str, is_shared_folder = secret_uid in vault.vault_data._shared_folders if is_record: + record = vault.vault_data.load_record(record_uid=secret_uid) + if not isinstance(record, TypedRecord): + raise ValueError("Unable to share application secret, only typed records can be shared") share_key_decrypted = vault.vault_data.get_record_key(record_uid=secret_uid) share_type = ApplicationShareType.SHARE_TYPE_RECORD secret_type_name = RECORD @@ -969,7 +973,7 @@ def _send_share_request(vault: vault_online.VaultOnline, app_uid: str, "Please remove already shared UIDs from your command and try again." ) else: - logger.error(f"Failed to share secrets: {kae}") + raise ValueError(f"Failed to share secrets: {kae}") return False @staticmethod diff --git a/keepercli-package/src/keepercli/helpers/ksm_utils.py b/keepercli-package/src/keepercli/helpers/ksm_utils.py index 68945a93..8f7bee88 100644 --- a/keepercli-package/src/keepercli/helpers/ksm_utils.py +++ b/keepercli-package/src/keepercli/helpers/ksm_utils.py @@ -12,9 +12,9 @@ def print_client_device_info(client_devices: list[ksm.ClientDevice]) -> None: f' Device Name: {client_device.name}\n' \ f' Short ID: {client_device.short_id}\n' \ f' Created On: {client_device.created_on}\n' \ - f' Expires On: {client_device.expires_on}\n' \ - f' First Access: {client_device.first_access}\n' \ - f' Last Access: {client_device.last_access}\n' \ + f' Expires On: {client_device.expires_on or "Never"}\n' \ + f' First Access: {client_device.first_access or "Never"}\n' \ + f' Last Access: {client_device.last_access or "Never"}\n' \ f' IP Lock: {client_device.ip_lock}\n' \ f' IP Address: {client_device.ip_address or "--"}' logger.info(client_devices_str) diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index 8b8b8f1d..d6d772e2 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -5,7 +5,6 @@ import hmac import re from datetime import timedelta -from token import OP from typing import Iterator, List, Optional from urllib import parse from urllib.parse import urlunparse @@ -102,8 +101,7 @@ def process_external_share(context: KeeperParams, expiration_period: timedelta, request.id = name request.isSelfDestruct = is_self_destruct - # TODO: uncomment when proto is updated - # request.isEditable = is_editable + request.isEditable = is_editable vault.keeper_auth.execute_auth_rest( rest_endpoint=EXTERNAL_SHARE_ADD_URL, diff --git a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py index c83b802e..a391978c 100644 --- a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: APIRequest.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'APIRequest.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -25,7 +17,7 @@ from . import enterprise_pb2 as enterprise__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x41PIRequest.proto\x12\x0e\x41uthentication\x1a\x10\x65nterprise.proto\"\xb0\x01\n\nApiRequest\x12 \n\x18\x65ncryptedTransmissionKey\x18\x01 \x01(\x0c\x12\x13\n\x0bpublicKeyId\x18\x02 \x01(\x05\x12\x0e\n\x06locale\x18\x03 \x01(\t\x12\x18\n\x10\x65ncryptedPayload\x18\x04 \x01(\x0c\x12\x16\n\x0e\x65ncryptionType\x18\x05 \x01(\x05\x12\x11\n\trecaptcha\x18\x06 \x01(\t\x12\x16\n\x0esubEnvironment\x18\x07 \x01(\t\"j\n\x11\x41piRequestPayload\x12\x0f\n\x07payload\x18\x01 \x01(\x0c\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x02 \x01(\x0c\x12\x11\n\ttimeToken\x18\x03 \x01(\x0c\x12\x12\n\napiVersion\x18\x04 \x01(\x05\"6\n\tTransform\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x01(\x0c\":\n\rDeviceRequest\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x12\n\ndeviceName\x18\x02 \x01(\t\"T\n\x0b\x41uthRequest\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x03 \x01(\x0c\"\xc3\x01\n\x14NewUserMinimumParams\x12\x19\n\x11minimumIterations\x18\x01 \x01(\x05\x12\x1a\n\x12passwordMatchRegex\x18\x02 \x03(\t\x12 \n\x18passwordMatchDescription\x18\x03 \x03(\t\x12\x1a\n\x12isEnterpriseDomain\x18\x04 \x01(\x08\x12\x1e\n\x16\x65nterpriseEccPublicKey\x18\x05 \x01(\x0c\x12\x16\n\x0e\x66orbidKeyType2\x18\x06 \x01(\x08\"\x89\x01\n\x0fPreLoginRequest\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12,\n\tloginType\x18\x02 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x16\n\x0etwoFactorToken\x18\x03 \x01(\x0c\"\x80\x02\n\x0cLoginRequest\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12,\n\tloginType\x18\x02 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x1f\n\x17\x61uthenticationHashPrime\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x04 \x01(\x0c\x12\x14\n\x0c\x61uthResponse\x18\x05 \x01(\x0c\x12\x16\n\x0emcEnterpriseId\x18\x06 \x01(\x05\x12\x12\n\npush_token\x18\x07 \x01(\t\x12\x10\n\x08platform\x18\x08 \x01(\t\"\\\n\x0e\x44\x65viceResponse\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12,\n\x06status\x18\x02 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"V\n\x04Salt\x12\x12\n\niterations\x18\x01 \x01(\x05\x12\x0c\n\x04salt\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\x05\x12\x0b\n\x03uid\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\" \n\x10TwoFactorChannel\x12\x0c\n\x04type\x18\x01 \x01(\x05\"\xe2\x02\n\x11StartLoginRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x04 \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x05 \x01(\x0c\x12,\n\tloginType\x18\x06 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x16\n\x0emcEnterpriseId\x18\x07 \x01(\x05\x12\x30\n\x0bloginMethod\x18\x08 \x01(\x0e\x32\x1b.Authentication.LoginMethod\x12\x15\n\rforceNewLogin\x18\t \x01(\x08\x12\x11\n\tcloneCode\x18\n \x01(\x0c\x12\x18\n\x10v2TwoFactorToken\x18\x0b \x01(\t\x12\x12\n\naccountUid\x18\x0c \x01(\x0c\"\xa7\x04\n\rLoginResponse\x12.\n\nloginState\x18\x01 \x01(\x0e\x32\x1a.Authentication.LoginState\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\x12\x17\n\x0fprimaryUsername\x18\x03 \x01(\t\x12\x18\n\x10\x65ncryptedDataKey\x18\x04 \x01(\x0c\x12\x42\n\x14\x65ncryptedDataKeyType\x18\x05 \x01(\x0e\x32$.Authentication.EncryptedDataKeyType\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x06 \x01(\x0c\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x07 \x01(\x0c\x12:\n\x10sessionTokenType\x18\x08 \x01(\x0e\x32 .Authentication.SessionTokenType\x12\x0f\n\x07message\x18\t \x01(\t\x12\x0b\n\x03url\x18\n \x01(\t\x12\x36\n\x08\x63hannels\x18\x0b \x03(\x0b\x32$.Authentication.TwoFactorChannelInfo\x12\"\n\x04salt\x18\x0c \x03(\x0b\x32\x14.Authentication.Salt\x12\x11\n\tcloneCode\x18\r \x01(\x0c\x12\x1a\n\x12stateSpecificValue\x18\x0e \x01(\t\x12\x18\n\x10ssoClientVersion\x18\x0f \x01(\t\x12 \n\x18sessionTokenTypeModifier\x18\x10 \x01(\t\"\x8c\x01\n\x0bSsoUserInfo\x12\x13\n\x0b\x63ompanyName\x18\x01 \x01(\t\x12\x13\n\x0bsamlRequest\x18\x02 \x01(\t\x12\x17\n\x0fsamlRequestType\x18\x03 \x01(\t\x12\x15\n\rssoDomainName\x18\x04 \x01(\t\x12\x10\n\x08loginUrl\x18\x05 \x01(\t\x12\x11\n\tlogoutUrl\x18\x06 \x01(\t\"\xd6\x01\n\x10PreLoginResponse\x12\x32\n\x0c\x64\x65viceStatus\x18\x01 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12\"\n\x04salt\x18\x02 \x03(\x0b\x32\x14.Authentication.Salt\x12\x38\n\x0eOBSOLETE_FIELD\x18\x03 \x03(\x0b\x32 .Authentication.TwoFactorChannel\x12\x30\n\x0bssoUserInfo\x18\x04 \x01(\x0b\x32\x1b.Authentication.SsoUserInfo\"&\n\x12LoginAsUserRequest\x12\x10\n\x08username\x18\x01 \x01(\t\"W\n\x13LoginAsUserResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12!\n\x19\x65ncryptedSharedAccountKey\x18\x02 \x01(\x0c\"\x84\x01\n\x17ValidateAuthHashRequest\x12\x36\n\x0epasswordMethod\x18\x01 \x01(\x0e\x32\x1e.Authentication.PasswordMethod\x12\x14\n\x0c\x61uthResponse\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x03 \x01(\x0c\"\xc4\x02\n\x14TwoFactorChannelInfo\x12\x39\n\x0b\x63hannelType\x18\x01 \x01(\x0e\x32$.Authentication.TwoFactorChannelType\x12\x13\n\x0b\x63hannel_uid\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hannelName\x18\x03 \x01(\t\x12\x11\n\tchallenge\x18\x04 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x05 \x03(\t\x12\x13\n\x0bphoneNumber\x18\x06 \x01(\t\x12:\n\rmaxExpiration\x18\x07 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\x12\x11\n\tcreatedOn\x18\x08 \x01(\x03\x12:\n\rlastFrequency\x18\t \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"d\n\x12TwoFactorDuoStatus\x12\x14\n\x0c\x63\x61pabilities\x18\x01 \x03(\t\x12\x13\n\x0bphoneNumber\x18\x02 \x01(\t\x12\x12\n\nenroll_url\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\"\xc7\x01\n\x13TwoFactorAddRequest\x12\x39\n\x0b\x63hannelType\x18\x01 \x01(\x0e\x32$.Authentication.TwoFactorChannelType\x12\x13\n\x0b\x63hannel_uid\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hannelName\x18\x03 \x01(\t\x12\x13\n\x0bphoneNumber\x18\x04 \x01(\t\x12\x36\n\x0b\x64uoPushType\x18\x05 \x01(\x0e\x32!.Authentication.TwoFactorPushType\"B\n\x16TwoFactorRenameRequest\x12\x13\n\x0b\x63hannel_uid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63hannelName\x18\x02 \x01(\t\"=\n\x14TwoFactorAddResponse\x12\x11\n\tchallenge\x18\x01 \x01(\t\x12\x12\n\nbackupKeys\x18\x02 \x03(\t\"-\n\x16TwoFactorDeleteRequest\x12\x13\n\x0b\x63hannel_uid\x18\x01 \x01(\x0c\"a\n\x15TwoFactorListResponse\x12\x36\n\x08\x63hannels\x18\x01 \x03(\x0b\x32$.Authentication.TwoFactorChannelInfo\x12\x10\n\x08\x65xpireOn\x18\x02 \x01(\x03\"Y\n TwoFactorUpdateExpirationRequest\x12\x35\n\x08\x65xpireIn\x18\x01 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"\xc9\x01\n\x18TwoFactorValidateRequest\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x35\n\tvalueType\x18\x02 \x01(\x0e\x32\".Authentication.TwoFactorValueType\x12\r\n\x05value\x18\x03 \x01(\t\x12\x13\n\x0b\x63hannel_uid\x18\x04 \x01(\x0c\x12\x35\n\x08\x65xpireIn\x18\x05 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"8\n\x19TwoFactorValidateResponse\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\"\xb8\x01\n\x18TwoFactorSendPushRequest\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x33\n\x08pushType\x18\x02 \x01(\x0e\x32!.Authentication.TwoFactorPushType\x12\x13\n\x0b\x63hannel_uid\x18\x03 \x01(\x0c\x12\x35\n\x08\x65xpireIn\x18\x04 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"\x83\x01\n\x07License\x12\x0f\n\x07\x63reated\x18\x01 \x01(\x03\x12\x12\n\nexpiration\x18\x02 \x01(\x03\x12\x34\n\rlicenseStatus\x18\x03 \x01(\x0e\x32\x1d.Authentication.LicenseStatus\x12\x0c\n\x04paid\x18\x04 \x01(\x08\x12\x0f\n\x07message\x18\x05 \x01(\t\"G\n\x0fOwnerlessRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12\x0e\n\x06status\x18\x03 \x01(\x05\"L\n\x10OwnerlessRecords\x12\x38\n\x0fownerlessRecord\x18\x01 \x03(\x0b\x32\x1f.Authentication.OwnerlessRecord\"\xd7\x01\n\x0fUserAuthRequest\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04salt\x18\x02 \x01(\x0c\x12\x12\n\niterations\x18\x03 \x01(\x05\x12\x1a\n\x12\x65ncryptedClientKey\x18\x04 \x01(\x0c\x12\x10\n\x08\x61uthHash\x18\x05 \x01(\x0c\x12\x18\n\x10\x65ncryptedDataKey\x18\x06 \x01(\x0c\x12,\n\tloginType\x18\x07 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x0c\n\x04name\x18\x08 \x01(\t\x12\x11\n\talgorithm\x18\t \x01(\x05\"\x19\n\nUidRequest\x12\x0b\n\x03uid\x18\x01 \x03(\x0c\"\xab\x01\n\x13\x44\x65viceUpdateRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"\x81\x01\n\x1dRegisterDeviceInRegionRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\"\xf8\x02\n\x13RegistrationRequest\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12\x38\n\x0fuserAuthRequest\x18\x02 \x01(\x0b\x32\x1f.Authentication.UserAuthRequest\x12\x1a\n\x12\x65ncryptedClientKey\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x04 \x01(\x0c\x12\x11\n\tpublicKey\x18\x05 \x01(\x0c\x12\x18\n\x10verificationCode\x18\x06 \x01(\t\x12\x1e\n\x16\x64\x65precatedAuthHashHash\x18\x07 \x01(\x0c\x12$\n\x1c\x64\x65precatedEncryptedClientKey\x18\x08 \x01(\x0c\x12%\n\x1d\x64\x65precatedEncryptedPrivateKey\x18\t \x01(\x0c\x12\"\n\x1a\x64\x65precatedEncryptionParams\x18\n \x01(\x0c\"\xd0\x01\n\x16\x43onvertUserToV3Request\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12\x38\n\x0fuserAuthRequest\x18\x02 \x01(\x0b\x32\x1f.Authentication.UserAuthRequest\x12\x1a\n\x12\x65ncryptedClientKey\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x04 \x01(\x0c\x12\x11\n\tpublicKey\x18\x05 \x01(\x0c\"$\n\x10RevisionResponse\x12\x10\n\x08revision\x18\x01 \x01(\x03\"&\n\x12\x43hangeEmailRequest\x12\x10\n\x08newEmail\x18\x01 \x01(\t\"8\n\x13\x43hangeEmailResponse\x12!\n\x19\x65ncryptedChangeEmailToken\x18\x01 \x01(\x0c\"6\n\x1d\x45mailVerificationLinkResponse\x12\x15\n\remailVerified\x18\x01 \x01(\x08\")\n\x0cSecurityData\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"@\n\x11SecurityScoreData\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"\x8b\x02\n\x13SecurityDataRequest\x12\x38\n\x12recordSecurityData\x18\x01 \x03(\x0b\x32\x1c.Authentication.SecurityData\x12@\n\x1amasterPasswordSecurityData\x18\x02 \x03(\x0b\x32\x1c.Authentication.SecurityData\x12\x34\n\x0e\x65ncryptionType\x18\x03 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x42\n\x17recordSecurityScoreData\x18\x04 \x03(\x0b\x32!.Authentication.SecurityScoreData\"\xb3\x02\n\x1dSecurityReportIncrementalData\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1b\n\x13\x63urrentSecurityData\x18\x02 \x01(\x0c\x12#\n\x1b\x63urrentSecurityDataRevision\x18\x03 \x01(\x03\x12\x17\n\x0foldSecurityData\x18\x04 \x01(\x0c\x12\x1f\n\x17oldSecurityDataRevision\x18\x05 \x01(\x03\x12?\n\x19\x63urrentDataEncryptionType\x18\x06 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12;\n\x15oldDataEncryptionType\x18\x07 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"\x9f\x02\n\x0eSecurityReport\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1b\n\x13\x65ncryptedReportData\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x11\n\ttwoFactor\x18\x04 \x01(\t\x12\x11\n\tlastLogin\x18\x05 \x01(\x03\x12\x1e\n\x16numberOfReusedPassword\x18\x06 \x01(\x05\x12T\n\x1dsecurityReportIncrementalData\x18\x07 \x03(\x0b\x32-.Authentication.SecurityReportIncrementalData\x12\x0e\n\x06userId\x18\x08 \x01(\x05\x12\x18\n\x10hasOldEncryption\x18\t \x01(\x08\"S\n\x19SecurityReportSaveRequest\x12\x36\n\x0esecurityReport\x18\x01 \x03(\x0b\x32\x1e.Authentication.SecurityReport\")\n\x15SecurityReportRequest\x12\x10\n\x08\x66romPage\x18\x01 \x01(\x03\"\xd9\x01\n\x16SecurityReportResponse\x12\x1c\n\x14\x65nterprisePrivateKey\x18\x01 \x01(\x0c\x12\x36\n\x0esecurityReport\x18\x02 \x03(\x0b\x32\x1e.Authentication.SecurityReport\x12\x14\n\x0c\x61sOfRevision\x18\x03 \x01(\x03\x12\x10\n\x08\x66romPage\x18\x04 \x01(\x03\x12\x0e\n\x06toPage\x18\x05 \x01(\x03\x12\x10\n\x08\x63omplete\x18\x06 \x01(\x08\x12\x1f\n\x17\x65nterpriseEccPrivateKey\x18\x07 \x01(\x0c\"\'\n\x16ReusedPasswordsRequest\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\">\n\x14SummaryConsoleReport\x12\x12\n\nreportType\x18\x01 \x01(\x05\x12\x12\n\nreportData\x18\x02 \x01(\x0c\"|\n\x12\x43hangeToKeyTypeOne\x12/\n\nobjectType\x18\x01 \x01(\x0e\x32\x1b.Authentication.ObjectTypes\x12\x12\n\nprimaryUid\x18\x02 \x01(\x0c\x12\x14\n\x0csecondaryUid\x18\x03 \x01(\x0c\x12\x0b\n\x03key\x18\x04 \x01(\x0c\"[\n\x19\x43hangeToKeyTypeOneRequest\x12>\n\x12\x63hangeToKeyTypeOne\x18\x01 \x03(\x0b\x32\".Authentication.ChangeToKeyTypeOne\"U\n\x18\x43hangeToKeyTypeOneStatus\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\"h\n\x1a\x43hangeToKeyTypeOneResponse\x12J\n\x18\x63hangeToKeyTypeOneStatus\x18\x01 \x03(\x0b\x32(.Authentication.ChangeToKeyTypeOneStatus\"\xb9\x01\n\x18GetChangeKeyTypesRequest\x12=\n\x10onlyTheseObjects\x18\x01 \x03(\x0e\x32#.Authentication.EncryptedObjectType\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x1a\n\x12includeRecommended\x18\x03 \x01(\x08\x12\x13\n\x0bincludeKeys\x18\x04 \x01(\x08\x12\x1e\n\x16includeAllowedKeyTypes\x18\x05 \x01(\x08\"\x82\x01\n\x19GetChangeKeyTypesResponse\x12+\n\x04keys\x18\x01 \x03(\x0b\x32\x1d.Authentication.ChangeKeyType\x12\x38\n\x0f\x61llowedKeyTypes\x18\x02 \x03(\x0b\x32\x1f.Authentication.AllowedKeyTypes\"\x81\x01\n\x0f\x41llowedKeyTypes\x12\x37\n\nobjectType\x18\x01 \x01(\x0e\x32#.Authentication.EncryptedObjectType\x12\x35\n\x0f\x61llowedKeyTypes\x18\x02 \x03(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"=\n\x0e\x43hangeKeyTypes\x12+\n\x04keys\x18\x01 \x03(\x0b\x32\x1d.Authentication.ChangeKeyType\"\xd6\x01\n\rChangeKeyType\x12\x37\n\nobjectType\x18\x01 \x01(\x0e\x32#.Authentication.EncryptedObjectType\x12\x0b\n\x03uid\x18\x02 \x01(\x0c\x12\x14\n\x0csecondaryUid\x18\x03 \x01(\x0c\x12\x0b\n\x03key\x18\x04 \x01(\x0c\x12-\n\x07keyType\x18\x05 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12-\n\x06status\x18\x06 \x01(\x0e\x32\x1d.Authentication.GenericStatus\"!\n\x06SetKey\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0b\n\x03key\x18\x02 \x01(\x0c\"5\n\rSetKeyRequest\x12$\n\x04keys\x18\x01 \x03(\x0b\x32\x16.Authentication.SetKey\"\x92\x05\n\x11\x43reateUserRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x14\n\x0c\x61uthVerifier\x18\x02 \x01(\x0c\x12\x18\n\x10\x65ncryptionParams\x18\x03 \x01(\x0c\x12\x14\n\x0crsaPublicKey\x18\x04 \x01(\x0c\x12\x1e\n\x16rsaEncryptedPrivateKey\x18\x05 \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\x06 \x01(\x0c\x12\x1e\n\x16\x65\x63\x63\x45ncryptedPrivateKey\x18\x07 \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x08 \x01(\x0c\x12\x1a\n\x12\x65ncryptedClientKey\x18\t \x01(\x0c\x12\x15\n\rclientVersion\x18\n \x01(\t\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x0b \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x0c \x01(\x0c\x12\x19\n\x11messageSessionUid\x18\r \x01(\x0c\x12\x17\n\x0finstallReferrer\x18\x0e \x01(\t\x12\x0e\n\x06mccMNC\x18\x0f \x01(\x05\x12\x0b\n\x03mfg\x18\x10 \x01(\t\x12\r\n\x05model\x18\x11 \x01(\t\x12\r\n\x05\x62rand\x18\x12 \x01(\t\x12\x0f\n\x07product\x18\x13 \x01(\t\x12\x0e\n\x06\x64\x65vice\x18\x14 \x01(\t\x12\x0f\n\x07\x63\x61rrier\x18\x15 \x01(\t\x12\x18\n\x10verificationCode\x18\x16 \x01(\t\x12\x42\n\x16\x65nterpriseRegistration\x18\x17 \x01(\x0b\x32\".Enterprise.EnterpriseRegistration\x12\"\n\x1a\x65ncryptedVerificationToken\x18\x18 \x01(\x0c\x12\x1e\n\x16\x65nterpriseUsersDataKey\x18\x19 \x01(\x0c\"W\n!NodeEnforcementAddOrUpdateRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x13\n\x0b\x65nforcement\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\"C\n\x1cNodeEnforcementRemoveRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x13\n\x0b\x65nforcement\x18\x02 \x01(\t\"\x9f\x01\n\x0f\x41piRequestByKey\x12\r\n\x05keyId\x18\x01 \x01(\x05\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x10\n\x08username\x18\x03 \x01(\t\x12\x0e\n\x06locale\x18\x04 \x01(\t\x12<\n\x11supportedLanguage\x18\x05 \x01(\x0e\x32!.Authentication.SupportedLanguage\x12\x0c\n\x04type\x18\x06 \x01(\x05\"\xc7\x01\n\x15\x41piRequestByKAtoKAKey\x12,\n\x0csourceRegion\x18\x01 \x01(\x0e\x32\x16.Authentication.Region\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12<\n\x11supportedLanguage\x18\x03 \x01(\x0e\x32!.Authentication.SupportedLanguage\x12\x31\n\x11\x64\x65stinationRegion\x18\x04 \x01(\x0e\x32\x16.Authentication.Region\".\n\x0fMemcacheRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06userId\x18\x02 \x01(\x05\".\n\x10MemcacheResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"w\n\x1cMasterPasswordReentryRequest\x12\x16\n\x0epbkdf2Password\x18\x01 \x01(\t\x12?\n\x06\x61\x63tion\x18\x02 \x01(\x0e\x32/.Authentication.MasterPasswordReentryActionType\"\\\n\x1dMasterPasswordReentryResponse\x12;\n\x06status\x18\x01 \x01(\x0e\x32+.Authentication.MasterPasswordReentryStatus\"_\n\x19\x44\x65viceRegistrationRequest\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x03 \x01(\x0c\"\x9a\x01\n\x19\x44\x65viceVerificationRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x1b\n\x13verificationChannel\x18\x03 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x04 \x01(\x0c\x12\x15\n\rclientVersion\x18\x05 \x01(\t\"\xb2\x01\n\x1a\x44\x65viceVerificationResponse\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x03 \x01(\x0c\x12\x15\n\rclientVersion\x18\x04 \x01(\t\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"\xc8\x01\n\x15\x44\x65viceApprovalRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x18\n\x10twoFactorChannel\x18\x02 \x01(\t\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x0e\n\x06locale\x18\x04 \x01(\t\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x05 \x01(\x0c\x12\x10\n\x08totpCode\x18\x06 \x01(\t\x12\x10\n\x08\x64\x65viceIp\x18\x07 \x01(\t\x12\x1d\n\x15\x64\x65viceTokenExpireDays\x18\x08 \x01(\t\"9\n\x16\x44\x65viceApprovalResponse\x12\x1f\n\x17\x65ncryptedTwoFactorToken\x18\x01 \x01(\x0c\"~\n\x14\x41pproveDeviceRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64\x65nyApproval\x18\x03 \x01(\x08\x12\x12\n\nlinkDevice\x18\x04 \x01(\x08\"E\n\x1a\x45nterpriseUserAliasRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"Y\n\x1d\x45nterpriseUserAddAliasRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\x0f\n\x07primary\x18\x03 \x01(\x08\"w\n\x1f\x45nterpriseUserAddAliasRequestV2\x12T\n\x1d\x65nterpriseUserAddAliasRequest\x18\x01 \x03(\x0b\x32-.Authentication.EnterpriseUserAddAliasRequest\"H\n\x1c\x45nterpriseUserAddAliasStatus\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06status\x18\x02 \x01(\t\"^\n\x1e\x45nterpriseUserAddAliasResponse\x12<\n\x06status\x18\x01 \x03(\x0b\x32,.Authentication.EnterpriseUserAddAliasStatus\"&\n\x06\x44\x65vice\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\"\\\n\x1cRegisterDeviceDataKeyRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x02 \x01(\x0c\"n\n)ValidateCreateUserVerificationCodeRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x18\n\x10verificationCode\x18\x03 \x01(\t\"\xa3\x01\n%ValidateDeviceVerificationCodeRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x18\n\x10verificationCode\x18\x03 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x04 \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x05 \x01(\x0c\"Y\n\x19SendSessionMessageRequest\x12\x19\n\x11messageSessionUid\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63ommand\x18\x02 \x01(\t\x12\x10\n\x08username\x18\x03 \x01(\t\"M\n\x11GlobalUserAccount\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\x12\x12\n\nregionName\x18\x03 \x01(\t\"7\n\x0f\x41\x63\x63ountUsername\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x12\n\ndateActive\x18\x02 \x01(\t\"P\n\x19SsoServiceProviderRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x0e\n\x06locale\x18\x03 \x01(\t\"a\n\x1aSsoServiceProviderResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05spUrl\x18\x02 \x01(\t\x12\x0f\n\x07isCloud\x18\x03 \x01(\x08\x12\x15\n\rclientVersion\x18\x04 \x01(\t\"4\n\x12UserSettingRequest\x12\x0f\n\x07setting\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"f\n\rThrottleState\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.Authentication.ThrottleType\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\r\n\x05state\x18\x04 \x01(\x08\"\xb5\x01\n\x0eThrottleState2\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x16\n\x0ekeyDescription\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\x18\n\x10valueDescription\x18\x04 \x01(\t\x12\x12\n\nidentifier\x18\x05 \x01(\t\x12\x0e\n\x06locked\x18\x06 \x01(\x08\x12\x1a\n\x12includedInAllClear\x18\x07 \x01(\x08\x12\x15\n\rexpireSeconds\x18\x08 \x01(\x05\"\x97\x01\n\x11\x44\x65viceInformation\x12\x10\n\x08\x64\x65viceId\x18\x01 \x01(\x03\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x11\n\tlastLogin\x18\x04 \x01(\x03\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"*\n\x0bUserSetting\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08\".\n\x12UserDataKeyRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x03(\x03\"+\n\x18UserDataKeyByNodeRequest\x12\x0f\n\x07nodeIds\x18\x01 \x03(\x03\"\x80\x01\n\x1b\x45nterpriseUserIdDataKeyPair\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x18\n\x10\x65ncryptedDataKey\x18\x02 \x01(\x0c\x12-\n\x07keyType\x18\x03 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"\x95\x01\n\x0bUserDataKey\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x0f\n\x07roleKey\x18\x02 \x01(\x0c\x12\x12\n\nprivateKey\x18\x03 \x01(\t\x12Q\n\x1c\x65nterpriseUserIdDataKeyPairs\x18\x04 \x03(\x0b\x32+.Authentication.EnterpriseUserIdDataKeyPair\"z\n\x13UserDataKeyResponse\x12\x31\n\x0cuserDataKeys\x18\x01 \x03(\x0b\x32\x1b.Authentication.UserDataKey\x12\x14\n\x0c\x61\x63\x63\x65ssDenied\x18\x02 \x03(\x03\x12\x1a\n\x12noEncryptedDataKey\x18\x03 \x03(\x03\"H\n)MasterPasswordRecoveryVerificationRequest\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\"U\n\x1cGetSecurityQuestionV3Request\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x18\n\x10verificationCode\x18\x02 \x01(\t\"r\n\x1dGetSecurityQuestionV3Response\x12\x18\n\x10securityQuestion\x18\x01 \x01(\t\x12\x15\n\rbackupKeyDate\x18\x02 \x01(\x03\x12\x0c\n\x04salt\x18\x03 \x01(\x0c\x12\x12\n\niterations\x18\x04 \x01(\x05\"n\n\x19GetDataKeyBackupV3Request\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x18\n\x10verificationCode\x18\x02 \x01(\t\x12\x1a\n\x12securityAnswerHash\x18\x03 \x01(\x0c\"v\n\rPasswordRules\x12\x10\n\x08ruleType\x18\x01 \x01(\t\x12\r\n\x05match\x18\x02 \x01(\x08\x12\x0f\n\x07pattern\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x0f\n\x07minimum\x18\x05 \x01(\x05\x12\r\n\x05value\x18\x06 \x01(\t\"\xc9\x02\n\x1aGetDataKeyBackupV3Response\x12\x15\n\rdataKeyBackup\x18\x01 \x01(\x0c\x12\x19\n\x11\x64\x61taKeyBackupDate\x18\x02 \x01(\x03\x12\x11\n\tpublicKey\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x04 \x01(\x0c\x12\x11\n\tclientKey\x18\x05 \x01(\x0c\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x06 \x01(\x0c\x12\x34\n\rpasswordRules\x18\x07 \x03(\x0b\x32\x1d.Authentication.PasswordRules\x12\x1a\n\x12passwordRulesIntro\x18\x08 \x01(\t\x12\x1f\n\x17minimumPbkdf2Iterations\x18\t \x01(\x05\x12$\n\x07keyType\x18\n \x01(\x0e\x32\x13.Enterprise.KeyType\")\n\x14GetPublicKeysRequest\x12\x11\n\tusernames\x18\x01 \x03(\t\"r\n\x11PublicKeyResponse\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x14\n\x0cpublicEccKey\x18\x03 \x01(\x0c\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x11\n\terrorCode\x18\x05 \x01(\t\"P\n\x15GetPublicKeysResponse\x12\x37\n\x0ckeyResponses\x18\x01 \x03(\x0b\x32!.Authentication.PublicKeyResponse\"F\n\x14SetEccKeyPairRequest\x12\x11\n\tpublicKey\x18\x01 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x02 \x01(\x0c\"I\n\x15SetEccKeyPairsRequest\x12\x30\n\x08teamKeys\x18\x01 \x03(\x0b\x32\x1e.Authentication.TeamEccKeyPair\"R\n\x16SetEccKeyPairsResponse\x12\x38\n\x08teamKeys\x18\x01 \x03(\x0b\x32&.Authentication.TeamEccKeyPairResponse\"Q\n\x0eTeamEccKeyPair\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x03 \x01(\x0c\"X\n\x16TeamEccKeyPairResponse\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12-\n\x06status\x18\x02 \x01(\x0e\x32\x1d.Authentication.GenericStatus\"D\n\x17GetKsmPublicKeysRequest\x12\x11\n\tclientIds\x18\x01 \x03(\x0c\x12\x16\n\x0e\x63ontrollerUids\x18\x02 \x03(\x0c\"U\n\x17\x44\x65vicePublicKeyResponse\x12\x10\n\x08\x63lientId\x18\x01 \x01(\x0c\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\"Y\n\x18GetKsmPublicKeysResponse\x12=\n\x0ckeyResponses\x18\x01 \x03(\x0b\x32\'.Authentication.DevicePublicKeyResponse\"X\n\x13\x41\x64\x64\x41ppSharesRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12+\n\x06shares\x18\x02 \x03(\x0b\x32\x1b.Authentication.AppShareAdd\">\n\x16RemoveAppSharesRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06shares\x18\x02 \x03(\x0c\"\x87\x01\n\x0b\x41ppShareAdd\x12\x11\n\tsecretUid\x18\x02 \x01(\x0c\x12\x37\n\tshareType\x18\x03 \x01(\x0e\x32$.Authentication.ApplicationShareType\x12\x1a\n\x12\x65ncryptedSecretKey\x18\x04 \x01(\x0c\x12\x10\n\x08\x65\x64itable\x18\x05 \x01(\x08\"\x89\x01\n\x08\x41ppShare\x12\x11\n\tsecretUid\x18\x01 \x01(\x0c\x12\x37\n\tshareType\x18\x02 \x01(\x0e\x32$.Authentication.ApplicationShareType\x12\x10\n\x08\x65\x64itable\x18\x03 \x01(\x08\x12\x11\n\tcreatedOn\x18\x04 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"\xd9\x01\n\x13\x41\x64\x64\x41ppClientRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0f\x65ncryptedAppKey\x18\x02 \x01(\x0c\x12\x10\n\x08\x63lientId\x18\x03 \x01(\x0c\x12\x0e\n\x06lockIp\x18\x04 \x01(\x08\x12\x1b\n\x13\x66irstAccessExpireOn\x18\x05 \x01(\x03\x12\x16\n\x0e\x61\x63\x63\x65ssExpireOn\x18\x06 \x01(\x03\x12\n\n\x02id\x18\x07 \x01(\t\x12\x30\n\rappClientType\x18\x08 \x01(\x0e\x32\x19.Enterprise.AppClientType\"@\n\x17RemoveAppClientsRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63lients\x18\x02 \x03(\x0c\"\x96\x01\n\x17\x41\x64\x64\x45xternalShareRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x02 \x01(\x0c\x12\x10\n\x08\x63lientId\x18\x03 \x01(\x0c\x12\x16\n\x0e\x61\x63\x63\x65ssExpireOn\x18\x04 \x01(\x03\x12\n\n\x02id\x18\x05 \x01(\t\x12\x16\n\x0eisSelfDestruct\x18\x06 \x01(\x08\"\x82\x02\n\tAppClient\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08\x63lientId\x18\x02 \x01(\x0c\x12\x11\n\tcreatedOn\x18\x03 \x01(\x03\x12\x13\n\x0b\x66irstAccess\x18\x04 \x01(\x03\x12\x12\n\nlastAccess\x18\x05 \x01(\x03\x12\x11\n\tpublicKey\x18\x06 \x01(\x0c\x12\x0e\n\x06lockIp\x18\x07 \x01(\x08\x12\x11\n\tipAddress\x18\x08 \x01(\t\x12\x1b\n\x13\x66irstAccessExpireOn\x18\t \x01(\x03\x12\x16\n\x0e\x61\x63\x63\x65ssExpireOn\x18\n \x01(\x03\x12\x30\n\rappClientType\x18\x0b \x01(\x0e\x32\x19.Enterprise.AppClientType\")\n\x11GetAppInfoRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x03(\x0c\"\x8e\x01\n\x07\x41ppInfo\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12(\n\x06shares\x18\x02 \x03(\x0b\x32\x18.Authentication.AppShare\x12*\n\x07\x63lients\x18\x03 \x03(\x0b\x32\x19.Authentication.AppClient\x12\x17\n\x0fisExternalShare\x18\x04 \x01(\x08\">\n\x12GetAppInfoResponse\x12(\n\x07\x61ppInfo\x18\x01 \x03(\x0b\x32\x17.Authentication.AppInfo\"\xd5\x01\n\x12\x41pplicationSummary\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x12\n\nlastAccess\x18\x02 \x01(\x03\x12\x14\n\x0crecordShares\x18\x03 \x01(\x05\x12\x14\n\x0c\x66olderShares\x18\x04 \x01(\x05\x12\x15\n\rfolderRecords\x18\x05 \x01(\x05\x12\x13\n\x0b\x63lientCount\x18\x06 \x01(\x05\x12\x1a\n\x12\x65xpiredClientCount\x18\x07 \x01(\x05\x12\x10\n\x08username\x18\x08 \x01(\t\x12\x0f\n\x07\x61ppData\x18\t \x01(\x0c\"`\n\x1eGetApplicationsSummaryResponse\x12>\n\x12\x61pplicationSummary\x18\x01 \x03(\x0b\x32\".Authentication.ApplicationSummary\"/\n\x1bGetVerificationTokenRequest\x12\x10\n\x08username\x18\x01 \x01(\t\"B\n\x1cGetVerificationTokenResponse\x12\"\n\x1a\x65ncryptedVerificationToken\x18\x01 \x01(\x0c\"\'\n\x16SendShareInviteRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\"\xc5\x01\n\x18TimeLimitedAccessRequest\x12\x12\n\naccountUid\x18\x01 \x03(\x0c\x12\x0f\n\x07teamUid\x18\x02 \x03(\x0c\x12\x11\n\trecordUid\x18\x03 \x03(\x0c\x12\x17\n\x0fsharedObjectUid\x18\x04 \x01(\x0c\x12\x44\n\x15timeLimitedAccessType\x18\x05 \x01(\x0e\x32%.Authentication.TimeLimitedAccessType\x12\x12\n\nexpiration\x18\x06 \x01(\x03\"7\n\x17TimeLimitedAccessStatus\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xf8\x01\n\x19TimeLimitedAccessResponse\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12\x41\n\x10userAccessStatus\x18\x02 \x03(\x0b\x32\'.Authentication.TimeLimitedAccessStatus\x12\x41\n\x10teamAccessStatus\x18\x03 \x03(\x0b\x32\'.Authentication.TimeLimitedAccessStatus\x12\x43\n\x12recordAccessStatus\x18\x04 \x03(\x0b\x32\'.Authentication.TimeLimitedAccessStatus\"+\n\x16RequestDownloadRequest\x12\x11\n\tfileNames\x18\x01 \x03(\t\"g\n\x17RequestDownloadResponse\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12+\n\tdownloads\x18\x03 \x03(\x0b\x32\x18.Authentication.Download\"D\n\x08\x44ownload\x12\x10\n\x08\x66ileName\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x19\n\x11successStatusCode\x18\x03 \x01(\x05\"#\n\x11\x44\x65leteUserRequest\x12\x0e\n\x06reason\x18\x01 \x01(\t\"\x84\x01\n\x1b\x43hangeMasterPasswordRequest\x12\x14\n\x0c\x61uthVerifier\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptionParams\x18\x02 \x01(\x0c\x12\x1b\n\x13\x66romServiceProvider\x18\x03 \x01(\x08\x12\x18\n\x10iterationsChange\x18\x04 \x01(\x08\"=\n\x1c\x43hangeMasterPasswordResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\"Y\n\x1b\x41\x63\x63ountRecoverySetupRequest\x12 \n\x18recoveryEncryptedDataKey\x18\x01 \x01(\x0c\x12\x18\n\x10recoveryAuthHash\x18\x02 \x01(\x0c\"\xac\x01\n!AccountRecoveryVerifyCodeResponse\x12\x34\n\rbackupKeyType\x18\x01 \x01(\x0e\x32\x1d.Authentication.BackupKeyType\x12\x15\n\rbackupKeyDate\x18\x02 \x01(\x03\x12\x18\n\x10securityQuestion\x18\x03 \x01(\t\x12\x0c\n\x04salt\x18\x04 \x01(\x0c\x12\x12\n\niterations\x18\x05 \x01(\x05\",\n\x1b\x45mergencyAccessLoginRequest\x12\r\n\x05owner\x18\x01 \x01(\t\"\xb5\x01\n\x1c\x45mergencyAccessLoginResponse\x12\x14\n\x0csessionToken\x18\x01 \x01(\x0c\x12%\n\x07\x64\x61taKey\x18\x02 \x01(\x0b\x32\x14.Enterprise.TypedKey\x12+\n\rrsaPrivateKey\x18\x03 \x01(\x0b\x32\x14.Enterprise.TypedKey\x12+\n\reccPrivateKey\x18\x04 \x01(\x0b\x32\x14.Enterprise.TypedKey\"\xb2\x01\n\x0bUserTeamKey\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x1b\n\x13\x65ncryptedTeamKeyRSA\x18\x04 \x01(\x0c\x12\x1a\n\x12\x65ncryptedTeamKeyEC\x18\x05 \x01(\x0c\x12-\n\x06status\x18\x06 \x01(\x0e\x32\x1d.Authentication.GenericStatus\")\n\x16GenericRequestResponse\x12\x0f\n\x07request\x18\x01 \x03(\x0c\"f\n\x1aPasskeyRegistrationRequest\x12H\n\x17\x61uthenticatorAttachment\x18\x01 \x01(\x0e\x32\'.Authentication.AuthenticatorAttachment\"P\n\x1bPasskeyRegistrationResponse\x12\x16\n\x0e\x63hallengeToken\x18\x01 \x01(\x0c\x12\x19\n\x11pkCreationOptions\x18\x02 \x01(\t\"\x84\x01\n\x1fPasskeyRegistrationFinalization\x12\x16\n\x0e\x63hallengeToken\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61uthenticatorResponse\x18\x02 \x01(\t\x12\x19\n\x0c\x66riendlyName\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_friendlyName\"\xda\x01\n\x1cPasskeyAuthenticationRequest\x12H\n\x17\x61uthenticatorAttachment\x18\x01 \x01(\x0e\x32\'.Authentication.AuthenticatorAttachment\x12\x36\n\x0epasskeyPurpose\x18\x02 \x01(\x0e\x32\x1e.Authentication.PasskeyPurpose\x12 \n\x13\x65ncryptedLoginToken\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_encryptedLoginToken\"\x8b\x01\n\x1dPasskeyAuthenticationResponse\x12\x18\n\x10pkRequestOptions\x18\x01 \x01(\t\x12\x16\n\x0e\x63hallengeToken\x18\x02 \x01(\x0c\x12 \n\x13\x65ncryptedLoginToken\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_encryptedLoginToken\"\xbf\x01\n\x18PasskeyValidationRequest\x12\x16\n\x0e\x63hallengeToken\x18\x01 \x01(\x0c\x12\x19\n\x11\x61ssertionResponse\x18\x02 \x01(\x0c\x12\x36\n\x0epasskeyPurpose\x18\x03 \x01(\x0e\x32\x1e.Authentication.PasskeyPurpose\x12 \n\x13\x65ncryptedLoginToken\x18\x04 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_encryptedLoginToken\"I\n\x19PasskeyValidationResponse\x12\x0f\n\x07isValid\x18\x01 \x01(\x08\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x02 \x01(\x0c\"h\n\x14UpdatePasskeyRequest\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x14\n\x0c\x63redentialId\x18\x02 \x01(\x0c\x12\x19\n\x0c\x66riendlyName\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_friendlyName\"-\n\x12PasskeyListRequest\x12\x17\n\x0fincludeDisabled\x18\x01 \x01(\x08\"\xa4\x01\n\x0bPasskeyInfo\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x14\n\x0c\x63redentialId\x18\x02 \x01(\x0c\x12\x14\n\x0c\x66riendlyName\x18\x03 \x01(\t\x12\x0e\n\x06\x41\x41GUID\x18\x04 \x01(\t\x12\x17\n\x0f\x63reatedAtMillis\x18\x05 \x01(\x03\x12\x16\n\x0elastUsedMillis\x18\x06 \x01(\x03\x12\x18\n\x10\x64isabledAtMillis\x18\x07 \x01(\x03\"G\n\x13PasskeyListResponse\x12\x30\n\x0bpasskeyInfo\x18\x01 \x03(\x0b\x32\x1b.Authentication.PasskeyInfo*\xb9\x02\n\x11SupportedLanguage\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x41RABIC\x10\x01\x12\x0b\n\x07\x42RITISH\x10\x02\x12\x0b\n\x07\x43HINESE\x10\x03\x12\x15\n\x11\x43HINESE_HONG_KONG\x10\x04\x12\x12\n\x0e\x43HINESE_TAIWAN\x10\x05\x12\t\n\x05\x44UTCH\x10\x06\x12\n\n\x06\x46RENCH\x10\x07\x12\n\n\x06GERMAN\x10\x08\x12\t\n\x05GREEK\x10\t\x12\n\n\x06HEBREW\x10\n\x12\x0b\n\x07ITALIAN\x10\x0b\x12\x0c\n\x08JAPANESE\x10\x0c\x12\n\n\x06KOREAN\x10\r\x12\n\n\x06POLISH\x10\x0e\x12\x0e\n\nPORTUGUESE\x10\x0f\x12\x15\n\x11PORTUGUESE_BRAZIL\x10\x10\x12\x0c\n\x08ROMANIAN\x10\x11\x12\x0b\n\x07RUSSIAN\x10\x12\x12\n\n\x06SLOVAK\x10\x13\x12\x0b\n\x07SPANISH\x10\x14*k\n\tLoginType\x12\n\n\x06NORMAL\x10\x00\x12\x07\n\x03SSO\x10\x01\x12\x07\n\x03\x42IO\x10\x02\x12\r\n\tALTERNATE\x10\x03\x12\x0b\n\x07OFFLINE\x10\x04\x12\x13\n\x0f\x46ORGOT_PASSWORD\x10\x05\x12\x0f\n\x0bPASSKEY_BIO\x10\x06*q\n\x0c\x44\x65viceStatus\x12\x19\n\x15\x44\x45VICE_NEEDS_APPROVAL\x10\x00\x12\r\n\tDEVICE_OK\x10\x01\x12\x1b\n\x17\x44\x45VICE_DISABLED_BY_USER\x10\x02\x12\x1a\n\x16\x44\x45VICE_LOCKED_BY_ADMIN\x10\x03*A\n\rLicenseStatus\x12\t\n\x05OTHER\x10\x00\x12\n\n\x06\x41\x43TIVE\x10\x01\x12\x0b\n\x07\x45XPIRED\x10\x02\x12\x0c\n\x08\x44ISABLED\x10\x03*7\n\x0b\x41\x63\x63ountType\x12\x0c\n\x08\x43ONSUMER\x10\x00\x12\n\n\x06\x46\x41MILY\x10\x01\x12\x0e\n\nENTERPRISE\x10\x02*\x9f\x02\n\x10SessionTokenType\x12\x12\n\x0eNO_RESTRICTION\x10\x00\x12\x14\n\x10\x41\x43\x43OUNT_RECOVERY\x10\x01\x12\x11\n\rSHARE_ACCOUNT\x10\x02\x12\x0c\n\x08PURCHASE\x10\x03\x12\x0c\n\x08RESTRICT\x10\x04\x12\x11\n\rACCEPT_INVITE\x10\x05\x12\x12\n\x0eSUPPORT_SERVER\x10\x06\x12\x17\n\x13\x45NTERPRISE_CREATION\x10\x07\x12\x1f\n\x1b\x45XPIRED_BUT_ALLOWED_TO_SYNC\x10\x08\x12\x18\n\x14\x41\x43\x43\x45PT_FAMILY_INVITE\x10\t\x12!\n\x1d\x45NTERPRISE_CREATION_PURCHASED\x10\n\x12\x14\n\x10\x45MERGENCY_ACCESS\x10\x0b*G\n\x07Version\x12\x13\n\x0finvalid_version\x10\x00\x12\x13\n\x0f\x64\x65\x66\x61ult_version\x10\x01\x12\x12\n\x0esecond_version\x10\x02*7\n\x1fMasterPasswordReentryActionType\x12\n\n\x06UNMASK\x10\x00\x12\x08\n\x04\x43OPY\x10\x01*l\n\x0bLoginMethod\x12\x17\n\x13INVALID_LOGINMETHOD\x10\x00\x12\x14\n\x10\x45XISTING_ACCOUNT\x10\x01\x12\x0e\n\nSSO_DOMAIN\x10\x02\x12\r\n\tAFTER_SSO\x10\x03\x12\x0f\n\x0bNEW_ACCOUNT\x10\x04*\xa5\x04\n\nLoginState\x12\x16\n\x12INVALID_LOGINSTATE\x10\x00\x12\x0e\n\nLOGGED_OUT\x10\x01\x12\x1c\n\x18\x44\x45VICE_APPROVAL_REQUIRED\x10\x02\x12\x11\n\rDEVICE_LOCKED\x10\x03\x12\x12\n\x0e\x41\x43\x43OUNT_LOCKED\x10\x04\x12\x19\n\x15\x44\x45VICE_ACCOUNT_LOCKED\x10\x05\x12\x0b\n\x07UPGRADE\x10\x06\x12\x13\n\x0fLICENSE_EXPIRED\x10\x07\x12\x13\n\x0fREGION_REDIRECT\x10\x08\x12\x16\n\x12REDIRECT_CLOUD_SSO\x10\t\x12\x17\n\x13REDIRECT_ONSITE_SSO\x10\n\x12\x10\n\x0cREQUIRES_2FA\x10\x0c\x12\x16\n\x12REQUIRES_AUTH_HASH\x10\r\x12\x15\n\x11REQUIRES_USERNAME\x10\x0e\x12\x19\n\x15\x41\x46TER_CLOUD_SSO_LOGIN\x10\x0f\x12\x1d\n\x19REQUIRES_ACCOUNT_CREATION\x10\x10\x12&\n\"REQUIRES_DEVICE_ENCRYPTED_DATA_KEY\x10\x11\x12\x17\n\x13LOGIN_TOKEN_EXPIRED\x10\x12\x12\x1e\n\x1aPASSKEY_INITIATE_CHALLENGE\x10\x13\x12\x19\n\x15PASSKEY_AUTH_REQUIRED\x10\x14\x12!\n\x1dPASSKEY_VERIFY_AUTHENTICATION\x10\x15\x12\r\n\tLOGGED_IN\x10\x63*k\n\x14\x45ncryptedDataKeyType\x12\n\n\x06NO_KEY\x10\x00\x12\x18\n\x14\x42Y_DEVICE_PUBLIC_KEY\x10\x01\x12\x0f\n\x0b\x42Y_PASSWORD\x10\x02\x12\x10\n\x0c\x42Y_ALTERNATE\x10\x03\x12\n\n\x06\x42Y_BIO\x10\x04*-\n\x0ePasswordMethod\x12\x0b\n\x07\x45NTERED\x10\x00\x12\x0e\n\nBIOMETRICS\x10\x01*\xb9\x01\n\x11TwoFactorPushType\x12\x14\n\x10TWO_FA_PUSH_NONE\x10\x00\x12\x13\n\x0fTWO_FA_PUSH_SMS\x10\x01\x12\x16\n\x12TWO_FA_PUSH_KEEPER\x10\x02\x12\x18\n\x14TWO_FA_PUSH_DUO_PUSH\x10\x03\x12\x18\n\x14TWO_FA_PUSH_DUO_TEXT\x10\x04\x12\x18\n\x14TWO_FA_PUSH_DUO_CALL\x10\x05\x12\x13\n\x0fTWO_FA_PUSH_DNA\x10\x06*\xc3\x01\n\x12TwoFactorValueType\x12\x14\n\x10TWO_FA_CODE_NONE\x10\x00\x12\x14\n\x10TWO_FA_CODE_TOTP\x10\x01\x12\x13\n\x0fTWO_FA_CODE_SMS\x10\x02\x12\x13\n\x0fTWO_FA_CODE_DUO\x10\x03\x12\x13\n\x0fTWO_FA_CODE_RSA\x10\x04\x12\x13\n\x0fTWO_FA_RESP_U2F\x10\x05\x12\x18\n\x14TWO_FA_RESP_WEBAUTHN\x10\x06\x12\x13\n\x0fTWO_FA_CODE_DNA\x10\x07*\xe1\x01\n\x14TwoFactorChannelType\x12\x12\n\x0eTWO_FA_CT_NONE\x10\x00\x12\x12\n\x0eTWO_FA_CT_TOTP\x10\x01\x12\x11\n\rTWO_FA_CT_SMS\x10\x02\x12\x11\n\rTWO_FA_CT_DUO\x10\x03\x12\x11\n\rTWO_FA_CT_RSA\x10\x04\x12\x14\n\x10TWO_FA_CT_BACKUP\x10\x05\x12\x11\n\rTWO_FA_CT_U2F\x10\x06\x12\x16\n\x12TWO_FA_CT_WEBAUTHN\x10\x07\x12\x14\n\x10TWO_FA_CT_KEEPER\x10\x08\x12\x11\n\rTWO_FA_CT_DNA\x10\t*\xab\x01\n\x13TwoFactorExpiration\x12\x1a\n\x16TWO_FA_EXP_IMMEDIATELY\x10\x00\x12\x18\n\x14TWO_FA_EXP_5_MINUTES\x10\x01\x12\x17\n\x13TWO_FA_EXP_12_HOURS\x10\x02\x12\x17\n\x13TWO_FA_EXP_24_HOURS\x10\x03\x12\x16\n\x12TWO_FA_EXP_30_DAYS\x10\x04\x12\x14\n\x10TWO_FA_EXP_NEVER\x10\x05*@\n\x0bLicenseType\x12\t\n\x05VAULT\x10\x00\x12\x08\n\x04\x43HAT\x10\x01\x12\x0b\n\x07STORAGE\x10\x02\x12\x0f\n\x0b\x42REACHWATCH\x10\x03*i\n\x0bObjectTypes\x12\n\n\x06RECORD\x10\x00\x12\x16\n\x12SHARED_FOLDER_USER\x10\x01\x12\x16\n\x12SHARED_FOLDER_TEAM\x10\x02\x12\x0f\n\x0bUSER_FOLDER\x10\x03\x12\r\n\tTEAM_USER\x10\x04*\xa1\x02\n\x13\x45ncryptedObjectType\x12\x13\n\x0f\x45OT_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x45OT_RECORD_KEY\x10\x01\x12\x1e\n\x1a\x45OT_SHARED_FOLDER_USER_KEY\x10\x02\x12\x1e\n\x1a\x45OT_SHARED_FOLDER_TEAM_KEY\x10\x03\x12\x15\n\x11\x45OT_TEAM_USER_KEY\x10\x04\x12\x17\n\x13\x45OT_USER_FOLDER_KEY\x10\x05\x12\x15\n\x11\x45OT_SECURITY_DATA\x10\x06\x12%\n!EOT_SECURITY_DATA_MASTER_PASSWORD\x10\x07\x12\x1c\n\x18\x45OT_EMERGENCY_ACCESS_KEY\x10\x08\x12\x15\n\x11\x45OT_V2_RECORD_KEY\x10\t*M\n\x1bMasterPasswordReentryStatus\x12\x0e\n\nMP_UNKNOWN\x10\x00\x12\x0e\n\nMP_SUCCESS\x10\x01\x12\x0e\n\nMP_FAILURE\x10\x02*`\n\x1b\x41lternateAuthenticationType\x12\x1d\n\x19\x41LTERNATE_MASTER_PASSWORD\x10\x00\x12\r\n\tBIOMETRIC\x10\x01\x12\x13\n\x0f\x41\x43\x43OUNT_RECOVER\x10\x02*\x9a\x02\n\x0cThrottleType\x12\x1b\n\x17PASSWORD_RETRY_THROTTLE\x10\x00\x12\"\n\x1ePASSWORD_RETRY_LEGACY_THROTTLE\x10\x01\x12\x13\n\x0fTWO_FA_THROTTLE\x10\x02\x12\x1a\n\x16TWO_FA_LEGACY_THROTTLE\x10\x03\x12\x15\n\x11QA_RETRY_THROTTLE\x10\x04\x12\x1c\n\x18\x41\x43\x43OUNT_RECOVER_THROTTLE\x10\x05\x12.\n*VALIDATE_DEVICE_VERIFICATION_CODE_THROTTLE\x10\x06\x12\x33\n/VALIDATE_CREATE_USER_VERIFICATION_CODE_THROTTLE\x10\x07*H\n\x06Region\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x06\n\x02\x65u\x10\x01\x12\x06\n\x02us\x10\x02\x12\t\n\x05usgov\x10\x03\x12\x06\n\x02\x61u\x10\x04\x12\x06\n\x02jp\x10\x05\x12\x06\n\x02\x63\x61\x10\x06*D\n\x14\x41pplicationShareType\x12\x15\n\x11SHARE_TYPE_RECORD\x10\x00\x12\x15\n\x11SHARE_TYPE_FOLDER\x10\x01*\xa4\x01\n\x15TimeLimitedAccessType\x12$\n INVALID_TIME_LIMITED_ACCESS_TYPE\x10\x00\x12\x19\n\x15USER_ACCESS_TO_RECORD\x10\x01\x12\'\n#USER_OR_TEAM_ACCESS_TO_SHAREDFOLDER\x10\x02\x12!\n\x1dRECORD_ACCESS_TO_SHAREDFOLDER\x10\x03*<\n\rBackupKeyType\x12\x12\n\x0e\x42KT_SEC_ANSWER\x10\x00\x12\x17\n\x13\x42KT_PASSPHRASE_HASH\x10\x01*W\n\rGenericStatus\x12\x0b\n\x07SUCCESS\x10\x00\x12\x12\n\x0eINVALID_OBJECT\x10\x01\x12\x12\n\x0e\x41LREADY_EXISTS\x10\x02\x12\x11\n\rACCESS_DENIED\x10\x03*N\n\x17\x41uthenticatorAttachment\x12\x12\n\x0e\x43ROSS_PLATFORM\x10\x00\x12\x0c\n\x08PLATFORM\x10\x01\x12\x11\n\rALL_SUPPORTED\x10\x02*-\n\x0ePasskeyPurpose\x12\x0c\n\x08PK_LOGIN\x10\x00\x12\r\n\tPK_REAUTH\x10\x01\x42*\n\x18\x63om.keepersecurity.protoB\x0e\x41uthenticationb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x41PIRequest.proto\x12\x0e\x41uthentication\x1a\x10\x65nterprise.proto\"\xb0\x01\n\nApiRequest\x12 \n\x18\x65ncryptedTransmissionKey\x18\x01 \x01(\x0c\x12\x13\n\x0bpublicKeyId\x18\x02 \x01(\x05\x12\x0e\n\x06locale\x18\x03 \x01(\t\x12\x18\n\x10\x65ncryptedPayload\x18\x04 \x01(\x0c\x12\x16\n\x0e\x65ncryptionType\x18\x05 \x01(\x05\x12\x11\n\trecaptcha\x18\x06 \x01(\t\x12\x16\n\x0esubEnvironment\x18\x07 \x01(\t\"j\n\x11\x41piRequestPayload\x12\x0f\n\x07payload\x18\x01 \x01(\x0c\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x02 \x01(\x0c\x12\x11\n\ttimeToken\x18\x03 \x01(\x0c\x12\x12\n\napiVersion\x18\x04 \x01(\x05\"6\n\tTransform\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x01(\x0c\"\xa0\x01\n\rDeviceRequest\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x16\n\x0e\x64\x65vicePlatform\x18\x03 \x01(\t\x12:\n\x10\x63lientFormFactor\x18\x04 \x01(\x0e\x32 .Authentication.ClientFormFactor\x12\x10\n\x08username\x18\x05 \x01(\t\"T\n\x0b\x41uthRequest\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x03 \x01(\x0c\"\xc3\x01\n\x14NewUserMinimumParams\x12\x19\n\x11minimumIterations\x18\x01 \x01(\x05\x12\x1a\n\x12passwordMatchRegex\x18\x02 \x03(\t\x12 \n\x18passwordMatchDescription\x18\x03 \x03(\t\x12\x1a\n\x12isEnterpriseDomain\x18\x04 \x01(\x08\x12\x1e\n\x16\x65nterpriseEccPublicKey\x18\x05 \x01(\x0c\x12\x16\n\x0e\x66orbidKeyType2\x18\x06 \x01(\x08\"\x89\x01\n\x0fPreLoginRequest\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12,\n\tloginType\x18\x02 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x16\n\x0etwoFactorToken\x18\x03 \x01(\x0c\"\x80\x02\n\x0cLoginRequest\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12,\n\tloginType\x18\x02 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x1f\n\x17\x61uthenticationHashPrime\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x04 \x01(\x0c\x12\x14\n\x0c\x61uthResponse\x18\x05 \x01(\x0c\x12\x16\n\x0emcEnterpriseId\x18\x06 \x01(\x05\x12\x12\n\npush_token\x18\x07 \x01(\t\x12\x10\n\x08platform\x18\x08 \x01(\t\"\\\n\x0e\x44\x65viceResponse\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12,\n\x06status\x18\x02 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"V\n\x04Salt\x12\x12\n\niterations\x18\x01 \x01(\x05\x12\x0c\n\x04salt\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\x05\x12\x0b\n\x03uid\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\" \n\x10TwoFactorChannel\x12\x0c\n\x04type\x18\x01 \x01(\x05\"\xfc\x02\n\x11StartLoginRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x04 \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x05 \x01(\x0c\x12,\n\tloginType\x18\x06 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x16\n\x0emcEnterpriseId\x18\x07 \x01(\x05\x12\x30\n\x0bloginMethod\x18\x08 \x01(\x0e\x32\x1b.Authentication.LoginMethod\x12\x15\n\rforceNewLogin\x18\t \x01(\x08\x12\x11\n\tcloneCode\x18\n \x01(\x0c\x12\x18\n\x10v2TwoFactorToken\x18\x0b \x01(\t\x12\x12\n\naccountUid\x18\x0c \x01(\x0c\x12\x18\n\x10\x66romSessionToken\x18\r \x01(\x0c\"\xa7\x04\n\rLoginResponse\x12.\n\nloginState\x18\x01 \x01(\x0e\x32\x1a.Authentication.LoginState\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\x12\x17\n\x0fprimaryUsername\x18\x03 \x01(\t\x12\x18\n\x10\x65ncryptedDataKey\x18\x04 \x01(\x0c\x12\x42\n\x14\x65ncryptedDataKeyType\x18\x05 \x01(\x0e\x32$.Authentication.EncryptedDataKeyType\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x06 \x01(\x0c\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x07 \x01(\x0c\x12:\n\x10sessionTokenType\x18\x08 \x01(\x0e\x32 .Authentication.SessionTokenType\x12\x0f\n\x07message\x18\t \x01(\t\x12\x0b\n\x03url\x18\n \x01(\t\x12\x36\n\x08\x63hannels\x18\x0b \x03(\x0b\x32$.Authentication.TwoFactorChannelInfo\x12\"\n\x04salt\x18\x0c \x03(\x0b\x32\x14.Authentication.Salt\x12\x11\n\tcloneCode\x18\r \x01(\x0c\x12\x1a\n\x12stateSpecificValue\x18\x0e \x01(\t\x12\x18\n\x10ssoClientVersion\x18\x0f \x01(\t\x12 \n\x18sessionTokenTypeModifier\x18\x10 \x01(\t\"_\n\x11SwitchListElement\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08\x66ullName\x18\x02 \x01(\t\x12\x14\n\x0c\x61uthRequired\x18\x03 \x01(\x08\x12\x10\n\x08isLinked\x18\x04 \x01(\x08\"I\n\x12SwitchListResponse\x12\x33\n\x08\x65lements\x18\x01 \x03(\x0b\x32!.Authentication.SwitchListElement\"\x8c\x01\n\x0bSsoUserInfo\x12\x13\n\x0b\x63ompanyName\x18\x01 \x01(\t\x12\x13\n\x0bsamlRequest\x18\x02 \x01(\t\x12\x17\n\x0fsamlRequestType\x18\x03 \x01(\t\x12\x15\n\rssoDomainName\x18\x04 \x01(\t\x12\x10\n\x08loginUrl\x18\x05 \x01(\t\x12\x11\n\tlogoutUrl\x18\x06 \x01(\t\"\xd6\x01\n\x10PreLoginResponse\x12\x32\n\x0c\x64\x65viceStatus\x18\x01 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12\"\n\x04salt\x18\x02 \x03(\x0b\x32\x14.Authentication.Salt\x12\x38\n\x0eOBSOLETE_FIELD\x18\x03 \x03(\x0b\x32 .Authentication.TwoFactorChannel\x12\x30\n\x0bssoUserInfo\x18\x04 \x01(\x0b\x32\x1b.Authentication.SsoUserInfo\"&\n\x12LoginAsUserRequest\x12\x10\n\x08username\x18\x01 \x01(\t\"W\n\x13LoginAsUserResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12!\n\x19\x65ncryptedSharedAccountKey\x18\x02 \x01(\x0c\"\x84\x01\n\x17ValidateAuthHashRequest\x12\x36\n\x0epasswordMethod\x18\x01 \x01(\x0e\x32\x1e.Authentication.PasswordMethod\x12\x14\n\x0c\x61uthResponse\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x03 \x01(\x0c\"\xc4\x02\n\x14TwoFactorChannelInfo\x12\x39\n\x0b\x63hannelType\x18\x01 \x01(\x0e\x32$.Authentication.TwoFactorChannelType\x12\x13\n\x0b\x63hannel_uid\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hannelName\x18\x03 \x01(\t\x12\x11\n\tchallenge\x18\x04 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x05 \x03(\t\x12\x13\n\x0bphoneNumber\x18\x06 \x01(\t\x12:\n\rmaxExpiration\x18\x07 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\x12\x11\n\tcreatedOn\x18\x08 \x01(\x03\x12:\n\rlastFrequency\x18\t \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"d\n\x12TwoFactorDuoStatus\x12\x14\n\x0c\x63\x61pabilities\x18\x01 \x03(\t\x12\x13\n\x0bphoneNumber\x18\x02 \x01(\t\x12\x12\n\nenroll_url\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\"\xc7\x01\n\x13TwoFactorAddRequest\x12\x39\n\x0b\x63hannelType\x18\x01 \x01(\x0e\x32$.Authentication.TwoFactorChannelType\x12\x13\n\x0b\x63hannel_uid\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hannelName\x18\x03 \x01(\t\x12\x13\n\x0bphoneNumber\x18\x04 \x01(\t\x12\x36\n\x0b\x64uoPushType\x18\x05 \x01(\x0e\x32!.Authentication.TwoFactorPushType\"B\n\x16TwoFactorRenameRequest\x12\x13\n\x0b\x63hannel_uid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63hannelName\x18\x02 \x01(\t\"=\n\x14TwoFactorAddResponse\x12\x11\n\tchallenge\x18\x01 \x01(\t\x12\x12\n\nbackupKeys\x18\x02 \x03(\t\"-\n\x16TwoFactorDeleteRequest\x12\x13\n\x0b\x63hannel_uid\x18\x01 \x01(\x0c\"a\n\x15TwoFactorListResponse\x12\x36\n\x08\x63hannels\x18\x01 \x03(\x0b\x32$.Authentication.TwoFactorChannelInfo\x12\x10\n\x08\x65xpireOn\x18\x02 \x01(\x03\"Y\n TwoFactorUpdateExpirationRequest\x12\x35\n\x08\x65xpireIn\x18\x01 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"\xc9\x01\n\x18TwoFactorValidateRequest\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x35\n\tvalueType\x18\x02 \x01(\x0e\x32\".Authentication.TwoFactorValueType\x12\r\n\x05value\x18\x03 \x01(\t\x12\x13\n\x0b\x63hannel_uid\x18\x04 \x01(\x0c\x12\x35\n\x08\x65xpireIn\x18\x05 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"8\n\x19TwoFactorValidateResponse\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\"\xb8\x01\n\x18TwoFactorSendPushRequest\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x33\n\x08pushType\x18\x02 \x01(\x0e\x32!.Authentication.TwoFactorPushType\x12\x13\n\x0b\x63hannel_uid\x18\x03 \x01(\x0c\x12\x35\n\x08\x65xpireIn\x18\x04 \x01(\x0e\x32#.Authentication.TwoFactorExpiration\"\x83\x01\n\x07License\x12\x0f\n\x07\x63reated\x18\x01 \x01(\x03\x12\x12\n\nexpiration\x18\x02 \x01(\x03\x12\x34\n\rlicenseStatus\x18\x03 \x01(\x0e\x32\x1d.Authentication.LicenseStatus\x12\x0c\n\x04paid\x18\x04 \x01(\x08\x12\x0f\n\x07message\x18\x05 \x01(\t\"G\n\x0fOwnerlessRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12\x0e\n\x06status\x18\x03 \x01(\x05\"L\n\x10OwnerlessRecords\x12\x38\n\x0fownerlessRecord\x18\x01 \x03(\x0b\x32\x1f.Authentication.OwnerlessRecord\"\xd7\x01\n\x0fUserAuthRequest\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04salt\x18\x02 \x01(\x0c\x12\x12\n\niterations\x18\x03 \x01(\x05\x12\x1a\n\x12\x65ncryptedClientKey\x18\x04 \x01(\x0c\x12\x10\n\x08\x61uthHash\x18\x05 \x01(\x0c\x12\x18\n\x10\x65ncryptedDataKey\x18\x06 \x01(\x0c\x12,\n\tloginType\x18\x07 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x0c\n\x04name\x18\x08 \x01(\t\x12\x11\n\talgorithm\x18\t \x01(\x05\"\x19\n\nUidRequest\x12\x0b\n\x03uid\x18\x01 \x03(\x0c\"\xff\x01\n\x13\x44\x65viceUpdateRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12\x16\n\x0e\x64\x65vicePlatform\x18\x06 \x01(\t\x12:\n\x10\x63lientFormFactor\x18\x07 \x01(\x0e\x32 .Authentication.ClientFormFactor\"\x80\x02\n\x14\x44\x65viceUpdateResponse\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12\x16\n\x0e\x64\x65vicePlatform\x18\x06 \x01(\t\x12:\n\x10\x63lientFormFactor\x18\x07 \x01(\x0e\x32 .Authentication.ClientFormFactor\"\xd5\x01\n\x1dRegisterDeviceInRegionRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12\x16\n\x0e\x64\x65vicePlatform\x18\x05 \x01(\t\x12:\n\x10\x63lientFormFactor\x18\x06 \x01(\x0e\x32 .Authentication.ClientFormFactor\"\xf8\x02\n\x13RegistrationRequest\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12\x38\n\x0fuserAuthRequest\x18\x02 \x01(\x0b\x32\x1f.Authentication.UserAuthRequest\x12\x1a\n\x12\x65ncryptedClientKey\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x04 \x01(\x0c\x12\x11\n\tpublicKey\x18\x05 \x01(\x0c\x12\x18\n\x10verificationCode\x18\x06 \x01(\t\x12\x1e\n\x16\x64\x65precatedAuthHashHash\x18\x07 \x01(\x0c\x12$\n\x1c\x64\x65precatedEncryptedClientKey\x18\x08 \x01(\x0c\x12%\n\x1d\x64\x65precatedEncryptedPrivateKey\x18\t \x01(\x0c\x12\"\n\x1a\x64\x65precatedEncryptionParams\x18\n \x01(\x0c\"\xd0\x01\n\x16\x43onvertUserToV3Request\x12\x30\n\x0b\x61uthRequest\x18\x01 \x01(\x0b\x32\x1b.Authentication.AuthRequest\x12\x38\n\x0fuserAuthRequest\x18\x02 \x01(\x0b\x32\x1f.Authentication.UserAuthRequest\x12\x1a\n\x12\x65ncryptedClientKey\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x04 \x01(\x0c\x12\x11\n\tpublicKey\x18\x05 \x01(\x0c\"$\n\x10RevisionResponse\x12\x10\n\x08revision\x18\x01 \x01(\x03\"&\n\x12\x43hangeEmailRequest\x12\x10\n\x08newEmail\x18\x01 \x01(\t\"8\n\x13\x43hangeEmailResponse\x12!\n\x19\x65ncryptedChangeEmailToken\x18\x01 \x01(\x0c\"6\n\x1d\x45mailVerificationLinkResponse\x12\x15\n\remailVerified\x18\x01 \x01(\x08\")\n\x0cSecurityData\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"@\n\x11SecurityScoreData\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"\x8b\x02\n\x13SecurityDataRequest\x12\x38\n\x12recordSecurityData\x18\x01 \x03(\x0b\x32\x1c.Authentication.SecurityData\x12@\n\x1amasterPasswordSecurityData\x18\x02 \x03(\x0b\x32\x1c.Authentication.SecurityData\x12\x34\n\x0e\x65ncryptionType\x18\x03 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x42\n\x17recordSecurityScoreData\x18\x04 \x03(\x0b\x32!.Authentication.SecurityScoreData\"\xc6\x02\n\x1dSecurityReportIncrementalData\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1b\n\x13\x63urrentSecurityData\x18\x02 \x01(\x0c\x12#\n\x1b\x63urrentSecurityDataRevision\x18\x03 \x01(\x03\x12\x17\n\x0foldSecurityData\x18\x04 \x01(\x0c\x12\x1f\n\x17oldSecurityDataRevision\x18\x05 \x01(\x03\x12?\n\x19\x63urrentDataEncryptionType\x18\x06 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12;\n\x15oldDataEncryptionType\x18\x07 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x11\n\trecordUid\x18\x08 \x01(\x0c\"\x9f\x02\n\x0eSecurityReport\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1b\n\x13\x65ncryptedReportData\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x11\n\ttwoFactor\x18\x04 \x01(\t\x12\x11\n\tlastLogin\x18\x05 \x01(\x03\x12\x1e\n\x16numberOfReusedPassword\x18\x06 \x01(\x05\x12T\n\x1dsecurityReportIncrementalData\x18\x07 \x03(\x0b\x32-.Authentication.SecurityReportIncrementalData\x12\x0e\n\x06userId\x18\x08 \x01(\x05\x12\x18\n\x10hasOldEncryption\x18\t \x01(\x08\"n\n\x19SecurityReportSaveRequest\x12\x36\n\x0esecurityReport\x18\x01 \x03(\x0b\x32\x1e.Authentication.SecurityReport\x12\x19\n\x11\x63ontinuationToken\x18\x02 \x01(\x0c\")\n\x15SecurityReportRequest\x12\x10\n\x08\x66romPage\x18\x01 \x01(\x03\"\xf5\x01\n\x16SecurityReportResponse\x12\x1c\n\x14\x65nterprisePrivateKey\x18\x01 \x01(\x0c\x12\x36\n\x0esecurityReport\x18\x02 \x03(\x0b\x32\x1e.Authentication.SecurityReport\x12\x14\n\x0c\x61sOfRevision\x18\x03 \x01(\x03\x12\x10\n\x08\x66romPage\x18\x04 \x01(\x03\x12\x0e\n\x06toPage\x18\x05 \x01(\x03\x12\x10\n\x08\x63omplete\x18\x06 \x01(\x08\x12\x1f\n\x17\x65nterpriseEccPrivateKey\x18\x07 \x01(\x0c\x12\x1a\n\x12hasIncrementalData\x18\x08 \x01(\x08\";\n\x1eIncrementalSecurityDataRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"\x92\x01\n\x1fIncrementalSecurityDataResponse\x12T\n\x1dsecurityReportIncrementalData\x18\x01 \x03(\x0b\x32-.Authentication.SecurityReportIncrementalData\x12\x19\n\x11\x63ontinuationToken\x18\x02 \x01(\x0c\"\'\n\x16ReusedPasswordsRequest\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\">\n\x14SummaryConsoleReport\x12\x12\n\nreportType\x18\x01 \x01(\x05\x12\x12\n\nreportData\x18\x02 \x01(\x0c\"|\n\x12\x43hangeToKeyTypeOne\x12/\n\nobjectType\x18\x01 \x01(\x0e\x32\x1b.Authentication.ObjectTypes\x12\x12\n\nprimaryUid\x18\x02 \x01(\x0c\x12\x14\n\x0csecondaryUid\x18\x03 \x01(\x0c\x12\x0b\n\x03key\x18\x04 \x01(\x0c\"[\n\x19\x43hangeToKeyTypeOneRequest\x12>\n\x12\x63hangeToKeyTypeOne\x18\x01 \x03(\x0b\x32\".Authentication.ChangeToKeyTypeOne\"U\n\x18\x43hangeToKeyTypeOneStatus\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\"h\n\x1a\x43hangeToKeyTypeOneResponse\x12J\n\x18\x63hangeToKeyTypeOneStatus\x18\x01 \x03(\x0b\x32(.Authentication.ChangeToKeyTypeOneStatus\"\xb9\x01\n\x18GetChangeKeyTypesRequest\x12=\n\x10onlyTheseObjects\x18\x01 \x03(\x0e\x32#.Authentication.EncryptedObjectType\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x1a\n\x12includeRecommended\x18\x03 \x01(\x08\x12\x13\n\x0bincludeKeys\x18\x04 \x01(\x08\x12\x1e\n\x16includeAllowedKeyTypes\x18\x05 \x01(\x08\"\x82\x01\n\x19GetChangeKeyTypesResponse\x12+\n\x04keys\x18\x01 \x03(\x0b\x32\x1d.Authentication.ChangeKeyType\x12\x38\n\x0f\x61llowedKeyTypes\x18\x02 \x03(\x0b\x32\x1f.Authentication.AllowedKeyTypes\"\x81\x01\n\x0f\x41llowedKeyTypes\x12\x37\n\nobjectType\x18\x01 \x01(\x0e\x32#.Authentication.EncryptedObjectType\x12\x35\n\x0f\x61llowedKeyTypes\x18\x02 \x03(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"=\n\x0e\x43hangeKeyTypes\x12+\n\x04keys\x18\x01 \x03(\x0b\x32\x1d.Authentication.ChangeKeyType\"\xd6\x01\n\rChangeKeyType\x12\x37\n\nobjectType\x18\x01 \x01(\x0e\x32#.Authentication.EncryptedObjectType\x12\x0b\n\x03uid\x18\x02 \x01(\x0c\x12\x14\n\x0csecondaryUid\x18\x03 \x01(\x0c\x12\x0b\n\x03key\x18\x04 \x01(\x0c\x12-\n\x07keyType\x18\x05 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12-\n\x06status\x18\x06 \x01(\x0e\x32\x1d.Authentication.GenericStatus\"!\n\x06SetKey\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0b\n\x03key\x18\x02 \x01(\x0c\"5\n\rSetKeyRequest\x12$\n\x04keys\x18\x01 \x03(\x0b\x32\x16.Authentication.SetKey\"\x92\x05\n\x11\x43reateUserRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x14\n\x0c\x61uthVerifier\x18\x02 \x01(\x0c\x12\x18\n\x10\x65ncryptionParams\x18\x03 \x01(\x0c\x12\x14\n\x0crsaPublicKey\x18\x04 \x01(\x0c\x12\x1e\n\x16rsaEncryptedPrivateKey\x18\x05 \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\x06 \x01(\x0c\x12\x1e\n\x16\x65\x63\x63\x45ncryptedPrivateKey\x18\x07 \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x08 \x01(\x0c\x12\x1a\n\x12\x65ncryptedClientKey\x18\t \x01(\x0c\x12\x15\n\rclientVersion\x18\n \x01(\t\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x0b \x01(\x0c\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x0c \x01(\x0c\x12\x19\n\x11messageSessionUid\x18\r \x01(\x0c\x12\x17\n\x0finstallReferrer\x18\x0e \x01(\t\x12\x0e\n\x06mccMNC\x18\x0f \x01(\x05\x12\x0b\n\x03mfg\x18\x10 \x01(\t\x12\r\n\x05model\x18\x11 \x01(\t\x12\r\n\x05\x62rand\x18\x12 \x01(\t\x12\x0f\n\x07product\x18\x13 \x01(\t\x12\x0e\n\x06\x64\x65vice\x18\x14 \x01(\t\x12\x0f\n\x07\x63\x61rrier\x18\x15 \x01(\t\x12\x18\n\x10verificationCode\x18\x16 \x01(\t\x12\x42\n\x16\x65nterpriseRegistration\x18\x17 \x01(\x0b\x32\".Enterprise.EnterpriseRegistration\x12\"\n\x1a\x65ncryptedVerificationToken\x18\x18 \x01(\x0c\x12\x1e\n\x16\x65nterpriseUsersDataKey\x18\x19 \x01(\x0c\"W\n!NodeEnforcementAddOrUpdateRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x13\n\x0b\x65nforcement\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\"C\n\x1cNodeEnforcementRemoveRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x13\n\x0b\x65nforcement\x18\x02 \x01(\t\"\x9f\x01\n\x0f\x41piRequestByKey\x12\r\n\x05keyId\x18\x01 \x01(\x05\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x10\n\x08username\x18\x03 \x01(\t\x12\x0e\n\x06locale\x18\x04 \x01(\t\x12<\n\x11supportedLanguage\x18\x05 \x01(\x0e\x32!.Authentication.SupportedLanguage\x12\x0c\n\x04type\x18\x06 \x01(\x05\"\xc7\x01\n\x15\x41piRequestByKAtoKAKey\x12,\n\x0csourceRegion\x18\x01 \x01(\x0e\x32\x16.Authentication.Region\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12<\n\x11supportedLanguage\x18\x03 \x01(\x0e\x32!.Authentication.SupportedLanguage\x12\x31\n\x11\x64\x65stinationRegion\x18\x04 \x01(\x0e\x32\x16.Authentication.Region\".\n\x0fMemcacheRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06userId\x18\x02 \x01(\x05\".\n\x10MemcacheResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"w\n\x1cMasterPasswordReentryRequest\x12\x16\n\x0epbkdf2Password\x18\x01 \x01(\t\x12?\n\x06\x61\x63tion\x18\x02 \x01(\x0e\x32/.Authentication.MasterPasswordReentryActionType\"\\\n\x1dMasterPasswordReentryResponse\x12;\n\x06status\x18\x01 \x01(\x0e\x32+.Authentication.MasterPasswordReentryStatus\"\xb3\x01\n\x19\x44\x65viceRegistrationRequest\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x03 \x01(\x0c\x12\x16\n\x0e\x64\x65vicePlatform\x18\x04 \x01(\t\x12:\n\x10\x63lientFormFactor\x18\x05 \x01(\x0e\x32 .Authentication.ClientFormFactor\"\x9a\x01\n\x19\x44\x65viceVerificationRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x1b\n\x13verificationChannel\x18\x03 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x04 \x01(\x0c\x12\x15\n\rclientVersion\x18\x05 \x01(\t\"\xb2\x01\n\x1a\x44\x65viceVerificationResponse\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x03 \x01(\x0c\x12\x15\n\rclientVersion\x18\x04 \x01(\t\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"\xc8\x01\n\x15\x44\x65viceApprovalRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x18\n\x10twoFactorChannel\x18\x02 \x01(\t\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x0e\n\x06locale\x18\x04 \x01(\t\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x05 \x01(\x0c\x12\x10\n\x08totpCode\x18\x06 \x01(\t\x12\x10\n\x08\x64\x65viceIp\x18\x07 \x01(\t\x12\x1d\n\x15\x64\x65viceTokenExpireDays\x18\x08 \x01(\t\"9\n\x16\x44\x65viceApprovalResponse\x12\x1f\n\x17\x65ncryptedTwoFactorToken\x18\x01 \x01(\x0c\"~\n\x14\x41pproveDeviceRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64\x65nyApproval\x18\x03 \x01(\x08\x12\x12\n\nlinkDevice\x18\x04 \x01(\x08\"E\n\x1a\x45nterpriseUserAliasRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"Y\n\x1d\x45nterpriseUserAddAliasRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\x0f\n\x07primary\x18\x03 \x01(\x08\"w\n\x1f\x45nterpriseUserAddAliasRequestV2\x12T\n\x1d\x65nterpriseUserAddAliasRequest\x18\x01 \x03(\x0b\x32-.Authentication.EnterpriseUserAddAliasRequest\"H\n\x1c\x45nterpriseUserAddAliasStatus\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06status\x18\x02 \x01(\t\"^\n\x1e\x45nterpriseUserAddAliasResponse\x12<\n\x06status\x18\x01 \x03(\x0b\x32,.Authentication.EnterpriseUserAddAliasStatus\"&\n\x06\x44\x65vice\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\"\\\n\x1cRegisterDeviceDataKeyRequest\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x02 \x01(\x0c\"n\n)ValidateCreateUserVerificationCodeRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x18\n\x10verificationCode\x18\x03 \x01(\t\"\xa3\x01\n%ValidateDeviceVerificationCodeRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x18\n\x10verificationCode\x18\x03 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x04 \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x05 \x01(\x0c\"Y\n\x19SendSessionMessageRequest\x12\x19\n\x11messageSessionUid\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63ommand\x18\x02 \x01(\t\x12\x10\n\x08username\x18\x03 \x01(\t\"M\n\x11GlobalUserAccount\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\x12\x12\n\nregionName\x18\x03 \x01(\t\"7\n\x0f\x41\x63\x63ountUsername\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x12\n\ndateActive\x18\x02 \x01(\t\"P\n\x19SsoServiceProviderRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x0e\n\x06locale\x18\x03 \x01(\t\"a\n\x1aSsoServiceProviderResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05spUrl\x18\x02 \x01(\t\x12\x0f\n\x07isCloud\x18\x03 \x01(\x08\x12\x15\n\rclientVersion\x18\x04 \x01(\t\"4\n\x12UserSettingRequest\x12\x0f\n\x07setting\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"f\n\rThrottleState\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.Authentication.ThrottleType\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\r\n\x05state\x18\x04 \x01(\x08\"\xb5\x01\n\x0eThrottleState2\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x16\n\x0ekeyDescription\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\x18\n\x10valueDescription\x18\x04 \x01(\t\x12\x12\n\nidentifier\x18\x05 \x01(\t\x12\x0e\n\x06locked\x18\x06 \x01(\x08\x12\x1a\n\x12includedInAllClear\x18\x07 \x01(\x08\x12\x15\n\rexpireSeconds\x18\x08 \x01(\x05\"\x97\x01\n\x11\x44\x65viceInformation\x12\x10\n\x08\x64\x65viceId\x18\x01 \x01(\x03\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x11\n\tlastLogin\x18\x04 \x01(\x03\x12\x32\n\x0c\x64\x65viceStatus\x18\x05 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\"*\n\x0bUserSetting\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08\".\n\x12UserDataKeyRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x03(\x03\"+\n\x18UserDataKeyByNodeRequest\x12\x0f\n\x07nodeIds\x18\x01 \x03(\x03\"\x80\x01\n\x1b\x45nterpriseUserIdDataKeyPair\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x18\n\x10\x65ncryptedDataKey\x18\x02 \x01(\x0c\x12-\n\x07keyType\x18\x03 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"\x95\x01\n\x0bUserDataKey\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x0f\n\x07roleKey\x18\x02 \x01(\x0c\x12\x12\n\nprivateKey\x18\x03 \x01(\t\x12Q\n\x1c\x65nterpriseUserIdDataKeyPairs\x18\x04 \x03(\x0b\x32+.Authentication.EnterpriseUserIdDataKeyPair\"z\n\x13UserDataKeyResponse\x12\x31\n\x0cuserDataKeys\x18\x01 \x03(\x0b\x32\x1b.Authentication.UserDataKey\x12\x14\n\x0c\x61\x63\x63\x65ssDenied\x18\x02 \x03(\x03\x12\x1a\n\x12noEncryptedDataKey\x18\x03 \x03(\x03\"H\n)MasterPasswordRecoveryVerificationRequest\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\"U\n\x1cGetSecurityQuestionV3Request\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x18\n\x10verificationCode\x18\x02 \x01(\t\"r\n\x1dGetSecurityQuestionV3Response\x12\x18\n\x10securityQuestion\x18\x01 \x01(\t\x12\x15\n\rbackupKeyDate\x18\x02 \x01(\x03\x12\x0c\n\x04salt\x18\x03 \x01(\x0c\x12\x12\n\niterations\x18\x04 \x01(\x05\"n\n\x19GetDataKeyBackupV3Request\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x01 \x01(\x0c\x12\x18\n\x10verificationCode\x18\x02 \x01(\t\x12\x1a\n\x12securityAnswerHash\x18\x03 \x01(\x0c\"v\n\rPasswordRules\x12\x10\n\x08ruleType\x18\x01 \x01(\t\x12\r\n\x05match\x18\x02 \x01(\x08\x12\x0f\n\x07pattern\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x0f\n\x07minimum\x18\x05 \x01(\x05\x12\r\n\x05value\x18\x06 \x01(\t\"\xc9\x02\n\x1aGetDataKeyBackupV3Response\x12\x15\n\rdataKeyBackup\x18\x01 \x01(\x0c\x12\x19\n\x11\x64\x61taKeyBackupDate\x18\x02 \x01(\x03\x12\x11\n\tpublicKey\x18\x03 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x04 \x01(\x0c\x12\x11\n\tclientKey\x18\x05 \x01(\x0c\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x06 \x01(\x0c\x12\x34\n\rpasswordRules\x18\x07 \x03(\x0b\x32\x1d.Authentication.PasswordRules\x12\x1a\n\x12passwordRulesIntro\x18\x08 \x01(\t\x12\x1f\n\x17minimumPbkdf2Iterations\x18\t \x01(\x05\x12$\n\x07keyType\x18\n \x01(\x0e\x32\x13.Enterprise.KeyType\")\n\x14GetPublicKeysRequest\x12\x11\n\tusernames\x18\x01 \x03(\t\"r\n\x11PublicKeyResponse\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x14\n\x0cpublicEccKey\x18\x03 \x01(\x0c\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x11\n\terrorCode\x18\x05 \x01(\t\"P\n\x15GetPublicKeysResponse\x12\x37\n\x0ckeyResponses\x18\x01 \x03(\x0b\x32!.Authentication.PublicKeyResponse\"F\n\x14SetEccKeyPairRequest\x12\x11\n\tpublicKey\x18\x01 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x02 \x01(\x0c\"I\n\x15SetEccKeyPairsRequest\x12\x30\n\x08teamKeys\x18\x01 \x03(\x0b\x32\x1e.Authentication.TeamEccKeyPair\"R\n\x16SetEccKeyPairsResponse\x12\x38\n\x08teamKeys\x18\x01 \x03(\x0b\x32&.Authentication.TeamEccKeyPairResponse\"Q\n\x0eTeamEccKeyPair\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x03 \x01(\x0c\"X\n\x16TeamEccKeyPairResponse\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12-\n\x06status\x18\x02 \x01(\x0e\x32\x1d.Authentication.GenericStatus\"D\n\x17GetKsmPublicKeysRequest\x12\x11\n\tclientIds\x18\x01 \x03(\x0c\x12\x16\n\x0e\x63ontrollerUids\x18\x02 \x03(\x0c\"U\n\x17\x44\x65vicePublicKeyResponse\x12\x10\n\x08\x63lientId\x18\x01 \x01(\x0c\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\"Y\n\x18GetKsmPublicKeysResponse\x12=\n\x0ckeyResponses\x18\x01 \x03(\x0b\x32\'.Authentication.DevicePublicKeyResponse\"X\n\x13\x41\x64\x64\x41ppSharesRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12+\n\x06shares\x18\x02 \x03(\x0b\x32\x1b.Authentication.AppShareAdd\">\n\x16RemoveAppSharesRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06shares\x18\x02 \x03(\x0c\"\x87\x01\n\x0b\x41ppShareAdd\x12\x11\n\tsecretUid\x18\x02 \x01(\x0c\x12\x37\n\tshareType\x18\x03 \x01(\x0e\x32$.Authentication.ApplicationShareType\x12\x1a\n\x12\x65ncryptedSecretKey\x18\x04 \x01(\x0c\x12\x10\n\x08\x65\x64itable\x18\x05 \x01(\x08\"\x89\x01\n\x08\x41ppShare\x12\x11\n\tsecretUid\x18\x01 \x01(\x0c\x12\x37\n\tshareType\x18\x02 \x01(\x0e\x32$.Authentication.ApplicationShareType\x12\x10\n\x08\x65\x64itable\x18\x03 \x01(\x08\x12\x11\n\tcreatedOn\x18\x04 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"\xd9\x01\n\x13\x41\x64\x64\x41ppClientRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0f\x65ncryptedAppKey\x18\x02 \x01(\x0c\x12\x10\n\x08\x63lientId\x18\x03 \x01(\x0c\x12\x0e\n\x06lockIp\x18\x04 \x01(\x08\x12\x1b\n\x13\x66irstAccessExpireOn\x18\x05 \x01(\x03\x12\x16\n\x0e\x61\x63\x63\x65ssExpireOn\x18\x06 \x01(\x03\x12\n\n\x02id\x18\x07 \x01(\t\x12\x30\n\rappClientType\x18\x08 \x01(\x0e\x32\x19.Enterprise.AppClientType\"@\n\x17RemoveAppClientsRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63lients\x18\x02 \x03(\x0c\"\xaa\x01\n\x17\x41\x64\x64\x45xternalShareRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x02 \x01(\x0c\x12\x10\n\x08\x63lientId\x18\x03 \x01(\x0c\x12\x16\n\x0e\x61\x63\x63\x65ssExpireOn\x18\x04 \x01(\x03\x12\n\n\x02id\x18\x05 \x01(\t\x12\x16\n\x0eisSelfDestruct\x18\x06 \x01(\x08\x12\x12\n\nisEditable\x18\x07 \x01(\x08\"\x93\x02\n\tAppClient\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08\x63lientId\x18\x02 \x01(\x0c\x12\x11\n\tcreatedOn\x18\x03 \x01(\x03\x12\x13\n\x0b\x66irstAccess\x18\x04 \x01(\x03\x12\x12\n\nlastAccess\x18\x05 \x01(\x03\x12\x11\n\tpublicKey\x18\x06 \x01(\x0c\x12\x0e\n\x06lockIp\x18\x07 \x01(\x08\x12\x11\n\tipAddress\x18\x08 \x01(\t\x12\x1b\n\x13\x66irstAccessExpireOn\x18\t \x01(\x03\x12\x16\n\x0e\x61\x63\x63\x65ssExpireOn\x18\n \x01(\x03\x12\x30\n\rappClientType\x18\x0b \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x0f\n\x07\x63\x61nEdit\x18\x0c \x01(\x08\")\n\x11GetAppInfoRequest\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x03(\x0c\"\x8e\x01\n\x07\x41ppInfo\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12(\n\x06shares\x18\x02 \x03(\x0b\x32\x18.Authentication.AppShare\x12*\n\x07\x63lients\x18\x03 \x03(\x0b\x32\x19.Authentication.AppClient\x12\x17\n\x0fisExternalShare\x18\x04 \x01(\x08\">\n\x12GetAppInfoResponse\x12(\n\x07\x61ppInfo\x18\x01 \x03(\x0b\x32\x17.Authentication.AppInfo\"\xd5\x01\n\x12\x41pplicationSummary\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x12\n\nlastAccess\x18\x02 \x01(\x03\x12\x14\n\x0crecordShares\x18\x03 \x01(\x05\x12\x14\n\x0c\x66olderShares\x18\x04 \x01(\x05\x12\x15\n\rfolderRecords\x18\x05 \x01(\x05\x12\x13\n\x0b\x63lientCount\x18\x06 \x01(\x05\x12\x1a\n\x12\x65xpiredClientCount\x18\x07 \x01(\x05\x12\x10\n\x08username\x18\x08 \x01(\t\x12\x0f\n\x07\x61ppData\x18\t \x01(\x0c\"`\n\x1eGetApplicationsSummaryResponse\x12>\n\x12\x61pplicationSummary\x18\x01 \x03(\x0b\x32\".Authentication.ApplicationSummary\"/\n\x1bGetVerificationTokenRequest\x12\x10\n\x08username\x18\x01 \x01(\t\"B\n\x1cGetVerificationTokenResponse\x12\"\n\x1a\x65ncryptedVerificationToken\x18\x01 \x01(\x0c\"\'\n\x16SendShareInviteRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\"\xc5\x01\n\x18TimeLimitedAccessRequest\x12\x12\n\naccountUid\x18\x01 \x03(\x0c\x12\x0f\n\x07teamUid\x18\x02 \x03(\x0c\x12\x11\n\trecordUid\x18\x03 \x03(\x0c\x12\x17\n\x0fsharedObjectUid\x18\x04 \x01(\x0c\x12\x44\n\x15timeLimitedAccessType\x18\x05 \x01(\x0e\x32%.Authentication.TimeLimitedAccessType\x12\x12\n\nexpiration\x18\x06 \x01(\x03\"7\n\x17TimeLimitedAccessStatus\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xf8\x01\n\x19TimeLimitedAccessResponse\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12\x41\n\x10userAccessStatus\x18\x02 \x03(\x0b\x32\'.Authentication.TimeLimitedAccessStatus\x12\x41\n\x10teamAccessStatus\x18\x03 \x03(\x0b\x32\'.Authentication.TimeLimitedAccessStatus\x12\x43\n\x12recordAccessStatus\x18\x04 \x03(\x0b\x32\'.Authentication.TimeLimitedAccessStatus\"+\n\x16RequestDownloadRequest\x12\x11\n\tfileNames\x18\x01 \x03(\t\"g\n\x17RequestDownloadResponse\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12+\n\tdownloads\x18\x03 \x03(\x0b\x32\x18.Authentication.Download\"D\n\x08\x44ownload\x12\x10\n\x08\x66ileName\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x19\n\x11successStatusCode\x18\x03 \x01(\x05\"#\n\x11\x44\x65leteUserRequest\x12\x0e\n\x06reason\x18\x01 \x01(\t\"\x84\x01\n\x1b\x43hangeMasterPasswordRequest\x12\x14\n\x0c\x61uthVerifier\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptionParams\x18\x02 \x01(\x0c\x12\x1b\n\x13\x66romServiceProvider\x18\x03 \x01(\x08\x12\x18\n\x10iterationsChange\x18\x04 \x01(\x08\"=\n\x1c\x43hangeMasterPasswordResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\"Y\n\x1b\x41\x63\x63ountRecoverySetupRequest\x12 \n\x18recoveryEncryptedDataKey\x18\x01 \x01(\x0c\x12\x18\n\x10recoveryAuthHash\x18\x02 \x01(\x0c\"\xac\x01\n!AccountRecoveryVerifyCodeResponse\x12\x34\n\rbackupKeyType\x18\x01 \x01(\x0e\x32\x1d.Authentication.BackupKeyType\x12\x15\n\rbackupKeyDate\x18\x02 \x01(\x03\x12\x18\n\x10securityQuestion\x18\x03 \x01(\t\x12\x0c\n\x04salt\x18\x04 \x01(\x0c\x12\x12\n\niterations\x18\x05 \x01(\x05\",\n\x1b\x45mergencyAccessLoginRequest\x12\r\n\x05owner\x18\x01 \x01(\t\"\xb5\x01\n\x1c\x45mergencyAccessLoginResponse\x12\x14\n\x0csessionToken\x18\x01 \x01(\x0c\x12%\n\x07\x64\x61taKey\x18\x02 \x01(\x0b\x32\x14.Enterprise.TypedKey\x12+\n\rrsaPrivateKey\x18\x03 \x01(\x0b\x32\x14.Enterprise.TypedKey\x12+\n\reccPrivateKey\x18\x04 \x01(\x0b\x32\x14.Enterprise.TypedKey\"\xb2\x01\n\x0bUserTeamKey\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x1b\n\x13\x65ncryptedTeamKeyRSA\x18\x04 \x01(\x0c\x12\x1a\n\x12\x65ncryptedTeamKeyEC\x18\x05 \x01(\x0c\x12-\n\x06status\x18\x06 \x01(\x0e\x32\x1d.Authentication.GenericStatus\")\n\x16GenericRequestResponse\x12\x0f\n\x07request\x18\x01 \x03(\x0c\"f\n\x1aPasskeyRegistrationRequest\x12H\n\x17\x61uthenticatorAttachment\x18\x01 \x01(\x0e\x32\'.Authentication.AuthenticatorAttachment\"P\n\x1bPasskeyRegistrationResponse\x12\x16\n\x0e\x63hallengeToken\x18\x01 \x01(\x0c\x12\x19\n\x11pkCreationOptions\x18\x02 \x01(\t\"\x84\x01\n\x1fPasskeyRegistrationFinalization\x12\x16\n\x0e\x63hallengeToken\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61uthenticatorResponse\x18\x02 \x01(\t\x12\x19\n\x0c\x66riendlyName\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_friendlyName\"\xb3\x02\n\x1cPasskeyAuthenticationRequest\x12H\n\x17\x61uthenticatorAttachment\x18\x01 \x01(\x0e\x32\'.Authentication.AuthenticatorAttachment\x12\x36\n\x0epasskeyPurpose\x18\x02 \x01(\x0e\x32\x1e.Authentication.PasskeyPurpose\x12\x15\n\rclientVersion\x18\x03 \x01(\t\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x04 \x01(\x0c\x12\x15\n\x08username\x18\x05 \x01(\tH\x00\x88\x01\x01\x12 \n\x13\x65ncryptedLoginToken\x18\x06 \x01(\x0cH\x01\x88\x01\x01\x42\x0b\n\t_usernameB\x16\n\x14_encryptedLoginToken\"\x8b\x01\n\x1dPasskeyAuthenticationResponse\x12\x18\n\x10pkRequestOptions\x18\x01 \x01(\t\x12\x16\n\x0e\x63hallengeToken\x18\x02 \x01(\x0c\x12 \n\x13\x65ncryptedLoginToken\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_encryptedLoginToken\"\xbf\x01\n\x18PasskeyValidationRequest\x12\x16\n\x0e\x63hallengeToken\x18\x01 \x01(\x0c\x12\x19\n\x11\x61ssertionResponse\x18\x02 \x01(\x0c\x12\x36\n\x0epasskeyPurpose\x18\x03 \x01(\x0e\x32\x1e.Authentication.PasskeyPurpose\x12 \n\x13\x65ncryptedLoginToken\x18\x04 \x01(\x0cH\x00\x88\x01\x01\x42\x16\n\x14_encryptedLoginToken\"I\n\x19PasskeyValidationResponse\x12\x0f\n\x07isValid\x18\x01 \x01(\x08\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x02 \x01(\x0c\"h\n\x14UpdatePasskeyRequest\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x14\n\x0c\x63redentialId\x18\x02 \x01(\x0c\x12\x19\n\x0c\x66riendlyName\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_friendlyName\"-\n\x12PasskeyListRequest\x12\x17\n\x0fincludeDisabled\x18\x01 \x01(\x08\"\xa4\x01\n\x0bPasskeyInfo\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x14\n\x0c\x63redentialId\x18\x02 \x01(\x0c\x12\x14\n\x0c\x66riendlyName\x18\x03 \x01(\t\x12\x0e\n\x06\x41\x41GUID\x18\x04 \x01(\t\x12\x17\n\x0f\x63reatedAtMillis\x18\x05 \x01(\x03\x12\x16\n\x0elastUsedMillis\x18\x06 \x01(\x03\x12\x18\n\x10\x64isabledAtMillis\x18\x07 \x01(\x03\"G\n\x13PasskeyListResponse\x12\x30\n\x0bpasskeyInfo\x18\x01 \x03(\x0b\x32\x1b.Authentication.PasskeyInfo\"C\n\x0fTranslationInfo\x12\x16\n\x0etranslationKey\x18\x01 \x01(\t\x12\x18\n\x10translationValue\x18\x02 \x01(\t\",\n\x12TranslationRequest\x12\x16\n\x0etranslationKey\x18\x01 \x03(\t\"O\n\x13TranslationResponse\x12\x38\n\x0ftranslationInfo\x18\x01 \x03(\x0b\x32\x1f.Authentication.TranslationInfo*\xd3\x02\n\x11SupportedLanguage\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x41RABIC\x10\x01\x12\x0b\n\x07\x42RITISH\x10\x02\x12\x0b\n\x07\x43HINESE\x10\x03\x12\x15\n\x11\x43HINESE_HONG_KONG\x10\x04\x12\x12\n\x0e\x43HINESE_TAIWAN\x10\x05\x12\t\n\x05\x44UTCH\x10\x06\x12\n\n\x06\x46RENCH\x10\x07\x12\n\n\x06GERMAN\x10\x08\x12\t\n\x05GREEK\x10\t\x12\n\n\x06HEBREW\x10\n\x12\x0b\n\x07ITALIAN\x10\x0b\x12\x0c\n\x08JAPANESE\x10\x0c\x12\n\n\x06KOREAN\x10\r\x12\n\n\x06POLISH\x10\x0e\x12\x0e\n\nPORTUGUESE\x10\x0f\x12\x15\n\x11PORTUGUESE_BRAZIL\x10\x10\x12\x0c\n\x08ROMANIAN\x10\x11\x12\x0b\n\x07RUSSIAN\x10\x12\x12\n\n\x06SLOVAK\x10\x13\x12\x0b\n\x07SPANISH\x10\x14\x12\x0b\n\x07\x46INNISH\x10\x15\x12\x0b\n\x07SWEDISH\x10\x16*k\n\tLoginType\x12\n\n\x06NORMAL\x10\x00\x12\x07\n\x03SSO\x10\x01\x12\x07\n\x03\x42IO\x10\x02\x12\r\n\tALTERNATE\x10\x03\x12\x0b\n\x07OFFLINE\x10\x04\x12\x13\n\x0f\x46ORGOT_PASSWORD\x10\x05\x12\x0f\n\x0bPASSKEY_BIO\x10\x06*q\n\x0c\x44\x65viceStatus\x12\x19\n\x15\x44\x45VICE_NEEDS_APPROVAL\x10\x00\x12\r\n\tDEVICE_OK\x10\x01\x12\x1b\n\x17\x44\x45VICE_DISABLED_BY_USER\x10\x02\x12\x1a\n\x16\x44\x45VICE_LOCKED_BY_ADMIN\x10\x03*A\n\rLicenseStatus\x12\t\n\x05OTHER\x10\x00\x12\n\n\x06\x41\x43TIVE\x10\x01\x12\x0b\n\x07\x45XPIRED\x10\x02\x12\x0c\n\x08\x44ISABLED\x10\x03*7\n\x0b\x41\x63\x63ountType\x12\x0c\n\x08\x43ONSUMER\x10\x00\x12\n\n\x06\x46\x41MILY\x10\x01\x12\x0e\n\nENTERPRISE\x10\x02*\x9f\x02\n\x10SessionTokenType\x12\x12\n\x0eNO_RESTRICTION\x10\x00\x12\x14\n\x10\x41\x43\x43OUNT_RECOVERY\x10\x01\x12\x11\n\rSHARE_ACCOUNT\x10\x02\x12\x0c\n\x08PURCHASE\x10\x03\x12\x0c\n\x08RESTRICT\x10\x04\x12\x11\n\rACCEPT_INVITE\x10\x05\x12\x12\n\x0eSUPPORT_SERVER\x10\x06\x12\x17\n\x13\x45NTERPRISE_CREATION\x10\x07\x12\x1f\n\x1b\x45XPIRED_BUT_ALLOWED_TO_SYNC\x10\x08\x12\x18\n\x14\x41\x43\x43\x45PT_FAMILY_INVITE\x10\t\x12!\n\x1d\x45NTERPRISE_CREATION_PURCHASED\x10\n\x12\x14\n\x10\x45MERGENCY_ACCESS\x10\x0b*G\n\x07Version\x12\x13\n\x0finvalid_version\x10\x00\x12\x13\n\x0f\x64\x65\x66\x61ult_version\x10\x01\x12\x12\n\x0esecond_version\x10\x02*7\n\x1fMasterPasswordReentryActionType\x12\n\n\x06UNMASK\x10\x00\x12\x08\n\x04\x43OPY\x10\x01*l\n\x0bLoginMethod\x12\x17\n\x13INVALID_LOGINMETHOD\x10\x00\x12\x14\n\x10\x45XISTING_ACCOUNT\x10\x01\x12\x0e\n\nSSO_DOMAIN\x10\x02\x12\r\n\tAFTER_SSO\x10\x03\x12\x0f\n\x0bNEW_ACCOUNT\x10\x04*\xbe\x04\n\nLoginState\x12\x16\n\x12INVALID_LOGINSTATE\x10\x00\x12\x0e\n\nLOGGED_OUT\x10\x01\x12\x1c\n\x18\x44\x45VICE_APPROVAL_REQUIRED\x10\x02\x12\x11\n\rDEVICE_LOCKED\x10\x03\x12\x12\n\x0e\x41\x43\x43OUNT_LOCKED\x10\x04\x12\x19\n\x15\x44\x45VICE_ACCOUNT_LOCKED\x10\x05\x12\x0b\n\x07UPGRADE\x10\x06\x12\x13\n\x0fLICENSE_EXPIRED\x10\x07\x12\x13\n\x0fREGION_REDIRECT\x10\x08\x12\x16\n\x12REDIRECT_CLOUD_SSO\x10\t\x12\x17\n\x13REDIRECT_ONSITE_SSO\x10\n\x12\x10\n\x0cREQUIRES_2FA\x10\x0c\x12\x16\n\x12REQUIRES_AUTH_HASH\x10\r\x12\x15\n\x11REQUIRES_USERNAME\x10\x0e\x12\x19\n\x15\x41\x46TER_CLOUD_SSO_LOGIN\x10\x0f\x12\x1d\n\x19REQUIRES_ACCOUNT_CREATION\x10\x10\x12&\n\"REQUIRES_DEVICE_ENCRYPTED_DATA_KEY\x10\x11\x12\x17\n\x13LOGIN_TOKEN_EXPIRED\x10\x12\x12\x1e\n\x1aPASSKEY_INITIATE_CHALLENGE\x10\x13\x12\x19\n\x15PASSKEY_AUTH_REQUIRED\x10\x14\x12!\n\x1dPASSKEY_VERIFY_AUTHENTICATION\x10\x15\x12\x17\n\x13\x41\x46TER_PASSKEY_LOGIN\x10\x16\x12\r\n\tLOGGED_IN\x10\x63*k\n\x14\x45ncryptedDataKeyType\x12\n\n\x06NO_KEY\x10\x00\x12\x18\n\x14\x42Y_DEVICE_PUBLIC_KEY\x10\x01\x12\x0f\n\x0b\x42Y_PASSWORD\x10\x02\x12\x10\n\x0c\x42Y_ALTERNATE\x10\x03\x12\n\n\x06\x42Y_BIO\x10\x04*-\n\x0ePasswordMethod\x12\x0b\n\x07\x45NTERED\x10\x00\x12\x0e\n\nBIOMETRICS\x10\x01*\xb9\x01\n\x11TwoFactorPushType\x12\x14\n\x10TWO_FA_PUSH_NONE\x10\x00\x12\x13\n\x0fTWO_FA_PUSH_SMS\x10\x01\x12\x16\n\x12TWO_FA_PUSH_KEEPER\x10\x02\x12\x18\n\x14TWO_FA_PUSH_DUO_PUSH\x10\x03\x12\x18\n\x14TWO_FA_PUSH_DUO_TEXT\x10\x04\x12\x18\n\x14TWO_FA_PUSH_DUO_CALL\x10\x05\x12\x13\n\x0fTWO_FA_PUSH_DNA\x10\x06*\xc3\x01\n\x12TwoFactorValueType\x12\x14\n\x10TWO_FA_CODE_NONE\x10\x00\x12\x14\n\x10TWO_FA_CODE_TOTP\x10\x01\x12\x13\n\x0fTWO_FA_CODE_SMS\x10\x02\x12\x13\n\x0fTWO_FA_CODE_DUO\x10\x03\x12\x13\n\x0fTWO_FA_CODE_RSA\x10\x04\x12\x13\n\x0fTWO_FA_RESP_U2F\x10\x05\x12\x18\n\x14TWO_FA_RESP_WEBAUTHN\x10\x06\x12\x13\n\x0fTWO_FA_CODE_DNA\x10\x07*\xe1\x01\n\x14TwoFactorChannelType\x12\x12\n\x0eTWO_FA_CT_NONE\x10\x00\x12\x12\n\x0eTWO_FA_CT_TOTP\x10\x01\x12\x11\n\rTWO_FA_CT_SMS\x10\x02\x12\x11\n\rTWO_FA_CT_DUO\x10\x03\x12\x11\n\rTWO_FA_CT_RSA\x10\x04\x12\x14\n\x10TWO_FA_CT_BACKUP\x10\x05\x12\x11\n\rTWO_FA_CT_U2F\x10\x06\x12\x16\n\x12TWO_FA_CT_WEBAUTHN\x10\x07\x12\x14\n\x10TWO_FA_CT_KEEPER\x10\x08\x12\x11\n\rTWO_FA_CT_DNA\x10\t*\xab\x01\n\x13TwoFactorExpiration\x12\x1a\n\x16TWO_FA_EXP_IMMEDIATELY\x10\x00\x12\x18\n\x14TWO_FA_EXP_5_MINUTES\x10\x01\x12\x17\n\x13TWO_FA_EXP_12_HOURS\x10\x02\x12\x17\n\x13TWO_FA_EXP_24_HOURS\x10\x03\x12\x16\n\x12TWO_FA_EXP_30_DAYS\x10\x04\x12\x14\n\x10TWO_FA_EXP_NEVER\x10\x05*@\n\x0bLicenseType\x12\t\n\x05VAULT\x10\x00\x12\x08\n\x04\x43HAT\x10\x01\x12\x0b\n\x07STORAGE\x10\x02\x12\x0f\n\x0b\x42REACHWATCH\x10\x03*i\n\x0bObjectTypes\x12\n\n\x06RECORD\x10\x00\x12\x16\n\x12SHARED_FOLDER_USER\x10\x01\x12\x16\n\x12SHARED_FOLDER_TEAM\x10\x02\x12\x0f\n\x0bUSER_FOLDER\x10\x03\x12\r\n\tTEAM_USER\x10\x04*\xa1\x02\n\x13\x45ncryptedObjectType\x12\x13\n\x0f\x45OT_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x45OT_RECORD_KEY\x10\x01\x12\x1e\n\x1a\x45OT_SHARED_FOLDER_USER_KEY\x10\x02\x12\x1e\n\x1a\x45OT_SHARED_FOLDER_TEAM_KEY\x10\x03\x12\x15\n\x11\x45OT_TEAM_USER_KEY\x10\x04\x12\x17\n\x13\x45OT_USER_FOLDER_KEY\x10\x05\x12\x15\n\x11\x45OT_SECURITY_DATA\x10\x06\x12%\n!EOT_SECURITY_DATA_MASTER_PASSWORD\x10\x07\x12\x1c\n\x18\x45OT_EMERGENCY_ACCESS_KEY\x10\x08\x12\x15\n\x11\x45OT_V2_RECORD_KEY\x10\t*M\n\x1bMasterPasswordReentryStatus\x12\x0e\n\nMP_UNKNOWN\x10\x00\x12\x0e\n\nMP_SUCCESS\x10\x01\x12\x0e\n\nMP_FAILURE\x10\x02*`\n\x1b\x41lternateAuthenticationType\x12\x1d\n\x19\x41LTERNATE_MASTER_PASSWORD\x10\x00\x12\r\n\tBIOMETRIC\x10\x01\x12\x13\n\x0f\x41\x43\x43OUNT_RECOVER\x10\x02*\x9a\x02\n\x0cThrottleType\x12\x1b\n\x17PASSWORD_RETRY_THROTTLE\x10\x00\x12\"\n\x1ePASSWORD_RETRY_LEGACY_THROTTLE\x10\x01\x12\x13\n\x0fTWO_FA_THROTTLE\x10\x02\x12\x1a\n\x16TWO_FA_LEGACY_THROTTLE\x10\x03\x12\x15\n\x11QA_RETRY_THROTTLE\x10\x04\x12\x1c\n\x18\x41\x43\x43OUNT_RECOVER_THROTTLE\x10\x05\x12.\n*VALIDATE_DEVICE_VERIFICATION_CODE_THROTTLE\x10\x06\x12\x33\n/VALIDATE_CREATE_USER_VERIFICATION_CODE_THROTTLE\x10\x07*H\n\x06Region\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x06\n\x02\x65u\x10\x01\x12\x06\n\x02us\x10\x02\x12\t\n\x05usgov\x10\x03\x12\x06\n\x02\x61u\x10\x04\x12\x06\n\x02jp\x10\x05\x12\x06\n\x02\x63\x61\x10\x06*D\n\x14\x41pplicationShareType\x12\x15\n\x11SHARE_TYPE_RECORD\x10\x00\x12\x15\n\x11SHARE_TYPE_FOLDER\x10\x01*\xa4\x01\n\x15TimeLimitedAccessType\x12$\n INVALID_TIME_LIMITED_ACCESS_TYPE\x10\x00\x12\x19\n\x15USER_ACCESS_TO_RECORD\x10\x01\x12\'\n#USER_OR_TEAM_ACCESS_TO_SHAREDFOLDER\x10\x02\x12!\n\x1dRECORD_ACCESS_TO_SHAREDFOLDER\x10\x03*<\n\rBackupKeyType\x12\x12\n\x0e\x42KT_SEC_ANSWER\x10\x00\x12\x17\n\x13\x42KT_PASSPHRASE_HASH\x10\x01*W\n\rGenericStatus\x12\x0b\n\x07SUCCESS\x10\x00\x12\x12\n\x0eINVALID_OBJECT\x10\x01\x12\x12\n\x0e\x41LREADY_EXISTS\x10\x02\x12\x11\n\rACCESS_DENIED\x10\x03*N\n\x17\x41uthenticatorAttachment\x12\x12\n\x0e\x43ROSS_PLATFORM\x10\x00\x12\x0c\n\x08PLATFORM\x10\x01\x12\x11\n\rALL_SUPPORTED\x10\x02*-\n\x0ePasskeyPurpose\x12\x0c\n\x08PK_LOGIN\x10\x00\x12\r\n\tPK_REAUTH\x10\x01*K\n\x10\x43lientFormFactor\x12\x0c\n\x08\x46\x46_EMPTY\x10\x00\x12\x0c\n\x08\x46\x46_PHONE\x10\x01\x12\r\n\tFF_TABLET\x10\x02\x12\x0c\n\x08\x46\x46_WATCH\x10\x03\x42*\n\x18\x63om.keepersecurity.protoB\x0e\x41uthenticationb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,384 +25,402 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\016Authentication' - _globals['_SUPPORTEDLANGUAGE']._serialized_start=19876 - _globals['_SUPPORTEDLANGUAGE']._serialized_end=20189 - _globals['_LOGINTYPE']._serialized_start=20191 - _globals['_LOGINTYPE']._serialized_end=20298 - _globals['_DEVICESTATUS']._serialized_start=20300 - _globals['_DEVICESTATUS']._serialized_end=20413 - _globals['_LICENSESTATUS']._serialized_start=20415 - _globals['_LICENSESTATUS']._serialized_end=20480 - _globals['_ACCOUNTTYPE']._serialized_start=20482 - _globals['_ACCOUNTTYPE']._serialized_end=20537 - _globals['_SESSIONTOKENTYPE']._serialized_start=20540 - _globals['_SESSIONTOKENTYPE']._serialized_end=20827 - _globals['_VERSION']._serialized_start=20829 - _globals['_VERSION']._serialized_end=20900 - _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_start=20902 - _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_end=20957 - _globals['_LOGINMETHOD']._serialized_start=20959 - _globals['_LOGINMETHOD']._serialized_end=21067 - _globals['_LOGINSTATE']._serialized_start=21070 - _globals['_LOGINSTATE']._serialized_end=21619 - _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_start=21621 - _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_end=21728 - _globals['_PASSWORDMETHOD']._serialized_start=21730 - _globals['_PASSWORDMETHOD']._serialized_end=21775 - _globals['_TWOFACTORPUSHTYPE']._serialized_start=21778 - _globals['_TWOFACTORPUSHTYPE']._serialized_end=21963 - _globals['_TWOFACTORVALUETYPE']._serialized_start=21966 - _globals['_TWOFACTORVALUETYPE']._serialized_end=22161 - _globals['_TWOFACTORCHANNELTYPE']._serialized_start=22164 - _globals['_TWOFACTORCHANNELTYPE']._serialized_end=22389 - _globals['_TWOFACTOREXPIRATION']._serialized_start=22392 - _globals['_TWOFACTOREXPIRATION']._serialized_end=22563 - _globals['_LICENSETYPE']._serialized_start=22565 - _globals['_LICENSETYPE']._serialized_end=22629 - _globals['_OBJECTTYPES']._serialized_start=22631 - _globals['_OBJECTTYPES']._serialized_end=22736 - _globals['_ENCRYPTEDOBJECTTYPE']._serialized_start=22739 - _globals['_ENCRYPTEDOBJECTTYPE']._serialized_end=23028 - _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_start=23030 - _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_end=23107 - _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_start=23109 - _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_end=23205 - _globals['_THROTTLETYPE']._serialized_start=23208 - _globals['_THROTTLETYPE']._serialized_end=23490 - _globals['_REGION']._serialized_start=23492 - _globals['_REGION']._serialized_end=23564 - _globals['_APPLICATIONSHARETYPE']._serialized_start=23566 - _globals['_APPLICATIONSHARETYPE']._serialized_end=23634 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=23637 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=23801 - _globals['_BACKUPKEYTYPE']._serialized_start=23803 - _globals['_BACKUPKEYTYPE']._serialized_end=23863 - _globals['_GENERICSTATUS']._serialized_start=23865 - _globals['_GENERICSTATUS']._serialized_end=23952 - _globals['_AUTHENTICATORATTACHMENT']._serialized_start=23954 - _globals['_AUTHENTICATORATTACHMENT']._serialized_end=24032 - _globals['_PASSKEYPURPOSE']._serialized_start=24034 - _globals['_PASSKEYPURPOSE']._serialized_end=24079 + _globals['_SUPPORTEDLANGUAGE']._serialized_start=21295 + _globals['_SUPPORTEDLANGUAGE']._serialized_end=21634 + _globals['_LOGINTYPE']._serialized_start=21636 + _globals['_LOGINTYPE']._serialized_end=21743 + _globals['_DEVICESTATUS']._serialized_start=21745 + _globals['_DEVICESTATUS']._serialized_end=21858 + _globals['_LICENSESTATUS']._serialized_start=21860 + _globals['_LICENSESTATUS']._serialized_end=21925 + _globals['_ACCOUNTTYPE']._serialized_start=21927 + _globals['_ACCOUNTTYPE']._serialized_end=21982 + _globals['_SESSIONTOKENTYPE']._serialized_start=21985 + _globals['_SESSIONTOKENTYPE']._serialized_end=22272 + _globals['_VERSION']._serialized_start=22274 + _globals['_VERSION']._serialized_end=22345 + _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_start=22347 + _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_end=22402 + _globals['_LOGINMETHOD']._serialized_start=22404 + _globals['_LOGINMETHOD']._serialized_end=22512 + _globals['_LOGINSTATE']._serialized_start=22515 + _globals['_LOGINSTATE']._serialized_end=23089 + _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_start=23091 + _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_end=23198 + _globals['_PASSWORDMETHOD']._serialized_start=23200 + _globals['_PASSWORDMETHOD']._serialized_end=23245 + _globals['_TWOFACTORPUSHTYPE']._serialized_start=23248 + _globals['_TWOFACTORPUSHTYPE']._serialized_end=23433 + _globals['_TWOFACTORVALUETYPE']._serialized_start=23436 + _globals['_TWOFACTORVALUETYPE']._serialized_end=23631 + _globals['_TWOFACTORCHANNELTYPE']._serialized_start=23634 + _globals['_TWOFACTORCHANNELTYPE']._serialized_end=23859 + _globals['_TWOFACTOREXPIRATION']._serialized_start=23862 + _globals['_TWOFACTOREXPIRATION']._serialized_end=24033 + _globals['_LICENSETYPE']._serialized_start=24035 + _globals['_LICENSETYPE']._serialized_end=24099 + _globals['_OBJECTTYPES']._serialized_start=24101 + _globals['_OBJECTTYPES']._serialized_end=24206 + _globals['_ENCRYPTEDOBJECTTYPE']._serialized_start=24209 + _globals['_ENCRYPTEDOBJECTTYPE']._serialized_end=24498 + _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_start=24500 + _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_end=24577 + _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_start=24579 + _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_end=24675 + _globals['_THROTTLETYPE']._serialized_start=24678 + _globals['_THROTTLETYPE']._serialized_end=24960 + _globals['_REGION']._serialized_start=24962 + _globals['_REGION']._serialized_end=25034 + _globals['_APPLICATIONSHARETYPE']._serialized_start=25036 + _globals['_APPLICATIONSHARETYPE']._serialized_end=25104 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=25107 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=25271 + _globals['_BACKUPKEYTYPE']._serialized_start=25273 + _globals['_BACKUPKEYTYPE']._serialized_end=25333 + _globals['_GENERICSTATUS']._serialized_start=25335 + _globals['_GENERICSTATUS']._serialized_end=25422 + _globals['_AUTHENTICATORATTACHMENT']._serialized_start=25424 + _globals['_AUTHENTICATORATTACHMENT']._serialized_end=25502 + _globals['_PASSKEYPURPOSE']._serialized_start=25504 + _globals['_PASSKEYPURPOSE']._serialized_end=25549 + _globals['_CLIENTFORMFACTOR']._serialized_start=25551 + _globals['_CLIENTFORMFACTOR']._serialized_end=25626 _globals['_APIREQUEST']._serialized_start=55 _globals['_APIREQUEST']._serialized_end=231 _globals['_APIREQUESTPAYLOAD']._serialized_start=233 _globals['_APIREQUESTPAYLOAD']._serialized_end=339 _globals['_TRANSFORM']._serialized_start=341 _globals['_TRANSFORM']._serialized_end=395 - _globals['_DEVICEREQUEST']._serialized_start=397 - _globals['_DEVICEREQUEST']._serialized_end=455 - _globals['_AUTHREQUEST']._serialized_start=457 - _globals['_AUTHREQUEST']._serialized_end=541 - _globals['_NEWUSERMINIMUMPARAMS']._serialized_start=544 - _globals['_NEWUSERMINIMUMPARAMS']._serialized_end=739 - _globals['_PRELOGINREQUEST']._serialized_start=742 - _globals['_PRELOGINREQUEST']._serialized_end=879 - _globals['_LOGINREQUEST']._serialized_start=882 - _globals['_LOGINREQUEST']._serialized_end=1138 - _globals['_DEVICERESPONSE']._serialized_start=1140 - _globals['_DEVICERESPONSE']._serialized_end=1232 - _globals['_SALT']._serialized_start=1234 - _globals['_SALT']._serialized_end=1320 - _globals['_TWOFACTORCHANNEL']._serialized_start=1322 - _globals['_TWOFACTORCHANNEL']._serialized_end=1354 - _globals['_STARTLOGINREQUEST']._serialized_start=1357 - _globals['_STARTLOGINREQUEST']._serialized_end=1711 - _globals['_LOGINRESPONSE']._serialized_start=1714 - _globals['_LOGINRESPONSE']._serialized_end=2265 - _globals['_SSOUSERINFO']._serialized_start=2268 - _globals['_SSOUSERINFO']._serialized_end=2408 - _globals['_PRELOGINRESPONSE']._serialized_start=2411 - _globals['_PRELOGINRESPONSE']._serialized_end=2625 - _globals['_LOGINASUSERREQUEST']._serialized_start=2627 - _globals['_LOGINASUSERREQUEST']._serialized_end=2665 - _globals['_LOGINASUSERRESPONSE']._serialized_start=2667 - _globals['_LOGINASUSERRESPONSE']._serialized_end=2754 - _globals['_VALIDATEAUTHHASHREQUEST']._serialized_start=2757 - _globals['_VALIDATEAUTHHASHREQUEST']._serialized_end=2889 - _globals['_TWOFACTORCHANNELINFO']._serialized_start=2892 - _globals['_TWOFACTORCHANNELINFO']._serialized_end=3216 - _globals['_TWOFACTORDUOSTATUS']._serialized_start=3218 - _globals['_TWOFACTORDUOSTATUS']._serialized_end=3318 - _globals['_TWOFACTORADDREQUEST']._serialized_start=3321 - _globals['_TWOFACTORADDREQUEST']._serialized_end=3520 - _globals['_TWOFACTORRENAMEREQUEST']._serialized_start=3522 - _globals['_TWOFACTORRENAMEREQUEST']._serialized_end=3588 - _globals['_TWOFACTORADDRESPONSE']._serialized_start=3590 - _globals['_TWOFACTORADDRESPONSE']._serialized_end=3651 - _globals['_TWOFACTORDELETEREQUEST']._serialized_start=3653 - _globals['_TWOFACTORDELETEREQUEST']._serialized_end=3698 - _globals['_TWOFACTORLISTRESPONSE']._serialized_start=3700 - _globals['_TWOFACTORLISTRESPONSE']._serialized_end=3797 - _globals['_TWOFACTORUPDATEEXPIRATIONREQUEST']._serialized_start=3799 - _globals['_TWOFACTORUPDATEEXPIRATIONREQUEST']._serialized_end=3888 - _globals['_TWOFACTORVALIDATEREQUEST']._serialized_start=3891 - _globals['_TWOFACTORVALIDATEREQUEST']._serialized_end=4092 - _globals['_TWOFACTORVALIDATERESPONSE']._serialized_start=4094 - _globals['_TWOFACTORVALIDATERESPONSE']._serialized_end=4150 - _globals['_TWOFACTORSENDPUSHREQUEST']._serialized_start=4153 - _globals['_TWOFACTORSENDPUSHREQUEST']._serialized_end=4337 - _globals['_LICENSE']._serialized_start=4340 - _globals['_LICENSE']._serialized_end=4471 - _globals['_OWNERLESSRECORD']._serialized_start=4473 - _globals['_OWNERLESSRECORD']._serialized_end=4544 - _globals['_OWNERLESSRECORDS']._serialized_start=4546 - _globals['_OWNERLESSRECORDS']._serialized_end=4622 - _globals['_USERAUTHREQUEST']._serialized_start=4625 - _globals['_USERAUTHREQUEST']._serialized_end=4840 - _globals['_UIDREQUEST']._serialized_start=4842 - _globals['_UIDREQUEST']._serialized_end=4867 - _globals['_DEVICEUPDATEREQUEST']._serialized_start=4870 - _globals['_DEVICEUPDATEREQUEST']._serialized_end=5041 - _globals['_REGISTERDEVICEINREGIONREQUEST']._serialized_start=5044 - _globals['_REGISTERDEVICEINREGIONREQUEST']._serialized_end=5173 - _globals['_REGISTRATIONREQUEST']._serialized_start=5176 - _globals['_REGISTRATIONREQUEST']._serialized_end=5552 - _globals['_CONVERTUSERTOV3REQUEST']._serialized_start=5555 - _globals['_CONVERTUSERTOV3REQUEST']._serialized_end=5763 - _globals['_REVISIONRESPONSE']._serialized_start=5765 - _globals['_REVISIONRESPONSE']._serialized_end=5801 - _globals['_CHANGEEMAILREQUEST']._serialized_start=5803 - _globals['_CHANGEEMAILREQUEST']._serialized_end=5841 - _globals['_CHANGEEMAILRESPONSE']._serialized_start=5843 - _globals['_CHANGEEMAILRESPONSE']._serialized_end=5899 - _globals['_EMAILVERIFICATIONLINKRESPONSE']._serialized_start=5901 - _globals['_EMAILVERIFICATIONLINKRESPONSE']._serialized_end=5955 - _globals['_SECURITYDATA']._serialized_start=5957 - _globals['_SECURITYDATA']._serialized_end=5998 - _globals['_SECURITYSCOREDATA']._serialized_start=6000 - _globals['_SECURITYSCOREDATA']._serialized_end=6064 - _globals['_SECURITYDATAREQUEST']._serialized_start=6067 - _globals['_SECURITYDATAREQUEST']._serialized_end=6334 - _globals['_SECURITYREPORTINCREMENTALDATA']._serialized_start=6337 - _globals['_SECURITYREPORTINCREMENTALDATA']._serialized_end=6644 - _globals['_SECURITYREPORT']._serialized_start=6647 - _globals['_SECURITYREPORT']._serialized_end=6934 - _globals['_SECURITYREPORTSAVEREQUEST']._serialized_start=6936 - _globals['_SECURITYREPORTSAVEREQUEST']._serialized_end=7019 - _globals['_SECURITYREPORTREQUEST']._serialized_start=7021 - _globals['_SECURITYREPORTREQUEST']._serialized_end=7062 - _globals['_SECURITYREPORTRESPONSE']._serialized_start=7065 - _globals['_SECURITYREPORTRESPONSE']._serialized_end=7282 - _globals['_REUSEDPASSWORDSREQUEST']._serialized_start=7284 - _globals['_REUSEDPASSWORDSREQUEST']._serialized_end=7323 - _globals['_SUMMARYCONSOLEREPORT']._serialized_start=7325 - _globals['_SUMMARYCONSOLEREPORT']._serialized_end=7387 - _globals['_CHANGETOKEYTYPEONE']._serialized_start=7389 - _globals['_CHANGETOKEYTYPEONE']._serialized_end=7513 - _globals['_CHANGETOKEYTYPEONEREQUEST']._serialized_start=7515 - _globals['_CHANGETOKEYTYPEONEREQUEST']._serialized_end=7606 - _globals['_CHANGETOKEYTYPEONESTATUS']._serialized_start=7608 - _globals['_CHANGETOKEYTYPEONESTATUS']._serialized_end=7693 - _globals['_CHANGETOKEYTYPEONERESPONSE']._serialized_start=7695 - _globals['_CHANGETOKEYTYPEONERESPONSE']._serialized_end=7799 - _globals['_GETCHANGEKEYTYPESREQUEST']._serialized_start=7802 - _globals['_GETCHANGEKEYTYPESREQUEST']._serialized_end=7987 - _globals['_GETCHANGEKEYTYPESRESPONSE']._serialized_start=7990 - _globals['_GETCHANGEKEYTYPESRESPONSE']._serialized_end=8120 - _globals['_ALLOWEDKEYTYPES']._serialized_start=8123 - _globals['_ALLOWEDKEYTYPES']._serialized_end=8252 - _globals['_CHANGEKEYTYPES']._serialized_start=8254 - _globals['_CHANGEKEYTYPES']._serialized_end=8315 - _globals['_CHANGEKEYTYPE']._serialized_start=8318 - _globals['_CHANGEKEYTYPE']._serialized_end=8532 - _globals['_SETKEY']._serialized_start=8534 - _globals['_SETKEY']._serialized_end=8567 - _globals['_SETKEYREQUEST']._serialized_start=8569 - _globals['_SETKEYREQUEST']._serialized_end=8622 - _globals['_CREATEUSERREQUEST']._serialized_start=8625 - _globals['_CREATEUSERREQUEST']._serialized_end=9283 - _globals['_NODEENFORCEMENTADDORUPDATEREQUEST']._serialized_start=9285 - _globals['_NODEENFORCEMENTADDORUPDATEREQUEST']._serialized_end=9372 - _globals['_NODEENFORCEMENTREMOVEREQUEST']._serialized_start=9374 - _globals['_NODEENFORCEMENTREMOVEREQUEST']._serialized_end=9441 - _globals['_APIREQUESTBYKEY']._serialized_start=9444 - _globals['_APIREQUESTBYKEY']._serialized_end=9603 - _globals['_APIREQUESTBYKATOKAKEY']._serialized_start=9606 - _globals['_APIREQUESTBYKATOKAKEY']._serialized_end=9805 - _globals['_MEMCACHEREQUEST']._serialized_start=9807 - _globals['_MEMCACHEREQUEST']._serialized_end=9853 - _globals['_MEMCACHERESPONSE']._serialized_start=9855 - _globals['_MEMCACHERESPONSE']._serialized_end=9901 - _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_start=9903 - _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_end=10022 - _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_start=10024 - _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_end=10116 - _globals['_DEVICEREGISTRATIONREQUEST']._serialized_start=10118 - _globals['_DEVICEREGISTRATIONREQUEST']._serialized_end=10213 - _globals['_DEVICEVERIFICATIONREQUEST']._serialized_start=10216 - _globals['_DEVICEVERIFICATIONREQUEST']._serialized_end=10370 - _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_start=10373 - _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_end=10551 - _globals['_DEVICEAPPROVALREQUEST']._serialized_start=10554 - _globals['_DEVICEAPPROVALREQUEST']._serialized_end=10754 - _globals['_DEVICEAPPROVALRESPONSE']._serialized_start=10756 - _globals['_DEVICEAPPROVALRESPONSE']._serialized_end=10813 - _globals['_APPROVEDEVICEREQUEST']._serialized_start=10815 - _globals['_APPROVEDEVICEREQUEST']._serialized_end=10941 - _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_start=10943 - _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_end=11012 - _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_start=11014 - _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_end=11103 - _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_start=11105 - _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_end=11224 - _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_start=11226 - _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_end=11298 - _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_start=11300 - _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_end=11394 - _globals['_DEVICE']._serialized_start=11396 - _globals['_DEVICE']._serialized_end=11434 - _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_start=11436 - _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_end=11528 - _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_start=11530 - _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_end=11640 - _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_start=11643 - _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_end=11806 - _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_start=11808 - _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_end=11897 - _globals['_GLOBALUSERACCOUNT']._serialized_start=11899 - _globals['_GLOBALUSERACCOUNT']._serialized_end=11976 - _globals['_ACCOUNTUSERNAME']._serialized_start=11978 - _globals['_ACCOUNTUSERNAME']._serialized_end=12033 - _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_start=12035 - _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_end=12115 - _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_start=12117 - _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_end=12214 - _globals['_USERSETTINGREQUEST']._serialized_start=12216 - _globals['_USERSETTINGREQUEST']._serialized_end=12268 - _globals['_THROTTLESTATE']._serialized_start=12270 - _globals['_THROTTLESTATE']._serialized_end=12372 - _globals['_THROTTLESTATE2']._serialized_start=12375 - _globals['_THROTTLESTATE2']._serialized_end=12556 - _globals['_DEVICEINFORMATION']._serialized_start=12559 - _globals['_DEVICEINFORMATION']._serialized_end=12710 - _globals['_USERSETTING']._serialized_start=12712 - _globals['_USERSETTING']._serialized_end=12754 - _globals['_USERDATAKEYREQUEST']._serialized_start=12756 - _globals['_USERDATAKEYREQUEST']._serialized_end=12802 - _globals['_USERDATAKEYBYNODEREQUEST']._serialized_start=12804 - _globals['_USERDATAKEYBYNODEREQUEST']._serialized_end=12847 - _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_start=12850 - _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_end=12978 - _globals['_USERDATAKEY']._serialized_start=12981 - _globals['_USERDATAKEY']._serialized_end=13130 - _globals['_USERDATAKEYRESPONSE']._serialized_start=13132 - _globals['_USERDATAKEYRESPONSE']._serialized_end=13254 - _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_start=13256 - _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_end=13328 - _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_start=13330 - _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_end=13415 - _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_start=13417 - _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_end=13531 - _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_start=13533 - _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_end=13643 - _globals['_PASSWORDRULES']._serialized_start=13645 - _globals['_PASSWORDRULES']._serialized_end=13763 - _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_start=13766 - _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_end=14095 - _globals['_GETPUBLICKEYSREQUEST']._serialized_start=14097 - _globals['_GETPUBLICKEYSREQUEST']._serialized_end=14138 - _globals['_PUBLICKEYRESPONSE']._serialized_start=14140 - _globals['_PUBLICKEYRESPONSE']._serialized_end=14254 - _globals['_GETPUBLICKEYSRESPONSE']._serialized_start=14256 - _globals['_GETPUBLICKEYSRESPONSE']._serialized_end=14336 - _globals['_SETECCKEYPAIRREQUEST']._serialized_start=14338 - _globals['_SETECCKEYPAIRREQUEST']._serialized_end=14408 - _globals['_SETECCKEYPAIRSREQUEST']._serialized_start=14410 - _globals['_SETECCKEYPAIRSREQUEST']._serialized_end=14483 - _globals['_SETECCKEYPAIRSRESPONSE']._serialized_start=14485 - _globals['_SETECCKEYPAIRSRESPONSE']._serialized_end=14567 - _globals['_TEAMECCKEYPAIR']._serialized_start=14569 - _globals['_TEAMECCKEYPAIR']._serialized_end=14650 - _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_start=14652 - _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_end=14740 - _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_start=14742 - _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_end=14810 - _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_start=14812 - _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_end=14897 - _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_start=14899 - _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_end=14988 - _globals['_ADDAPPSHARESREQUEST']._serialized_start=14990 - _globals['_ADDAPPSHARESREQUEST']._serialized_end=15078 - _globals['_REMOVEAPPSHARESREQUEST']._serialized_start=15080 - _globals['_REMOVEAPPSHARESREQUEST']._serialized_end=15142 - _globals['_APPSHAREADD']._serialized_start=15145 - _globals['_APPSHAREADD']._serialized_end=15280 - _globals['_APPSHARE']._serialized_start=15283 - _globals['_APPSHARE']._serialized_end=15420 - _globals['_ADDAPPCLIENTREQUEST']._serialized_start=15423 - _globals['_ADDAPPCLIENTREQUEST']._serialized_end=15640 - _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_start=15642 - _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_end=15706 - _globals['_ADDEXTERNALSHAREREQUEST']._serialized_start=15709 - _globals['_ADDEXTERNALSHAREREQUEST']._serialized_end=15859 - _globals['_APPCLIENT']._serialized_start=15862 - _globals['_APPCLIENT']._serialized_end=16120 - _globals['_GETAPPINFOREQUEST']._serialized_start=16122 - _globals['_GETAPPINFOREQUEST']._serialized_end=16163 - _globals['_APPINFO']._serialized_start=16166 - _globals['_APPINFO']._serialized_end=16308 - _globals['_GETAPPINFORESPONSE']._serialized_start=16310 - _globals['_GETAPPINFORESPONSE']._serialized_end=16372 - _globals['_APPLICATIONSUMMARY']._serialized_start=16375 - _globals['_APPLICATIONSUMMARY']._serialized_end=16588 - _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_start=16590 - _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_end=16686 - _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_start=16688 - _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_end=16735 - _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_start=16737 - _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_end=16803 - _globals['_SENDSHAREINVITEREQUEST']._serialized_start=16805 - _globals['_SENDSHAREINVITEREQUEST']._serialized_end=16844 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=16847 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=17044 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=17046 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=17101 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=17104 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=17352 - _globals['_REQUESTDOWNLOADREQUEST']._serialized_start=17354 - _globals['_REQUESTDOWNLOADREQUEST']._serialized_end=17397 - _globals['_REQUESTDOWNLOADRESPONSE']._serialized_start=17399 - _globals['_REQUESTDOWNLOADRESPONSE']._serialized_end=17502 - _globals['_DOWNLOAD']._serialized_start=17504 - _globals['_DOWNLOAD']._serialized_end=17572 - _globals['_DELETEUSERREQUEST']._serialized_start=17574 - _globals['_DELETEUSERREQUEST']._serialized_end=17609 - _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_start=17612 - _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_end=17744 - _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_start=17746 - _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_end=17807 - _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_start=17809 - _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_end=17898 - _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_start=17901 - _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_end=18073 - _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_start=18075 - _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_end=18119 - _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_start=18122 - _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_end=18303 - _globals['_USERTEAMKEY']._serialized_start=18306 - _globals['_USERTEAMKEY']._serialized_end=18484 - _globals['_GENERICREQUESTRESPONSE']._serialized_start=18486 - _globals['_GENERICREQUESTRESPONSE']._serialized_end=18527 - _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_start=18529 - _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_end=18631 - _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_start=18633 - _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_end=18713 - _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_start=18716 - _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_end=18848 - _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_start=18851 - _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_end=19069 - _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_start=19072 - _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_end=19211 - _globals['_PASSKEYVALIDATIONREQUEST']._serialized_start=19214 - _globals['_PASSKEYVALIDATIONREQUEST']._serialized_end=19405 - _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_start=19407 - _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_end=19480 - _globals['_UPDATEPASSKEYREQUEST']._serialized_start=19482 - _globals['_UPDATEPASSKEYREQUEST']._serialized_end=19586 - _globals['_PASSKEYLISTREQUEST']._serialized_start=19588 - _globals['_PASSKEYLISTREQUEST']._serialized_end=19633 - _globals['_PASSKEYINFO']._serialized_start=19636 - _globals['_PASSKEYINFO']._serialized_end=19800 - _globals['_PASSKEYLISTRESPONSE']._serialized_start=19802 - _globals['_PASSKEYLISTRESPONSE']._serialized_end=19873 + _globals['_DEVICEREQUEST']._serialized_start=398 + _globals['_DEVICEREQUEST']._serialized_end=558 + _globals['_AUTHREQUEST']._serialized_start=560 + _globals['_AUTHREQUEST']._serialized_end=644 + _globals['_NEWUSERMINIMUMPARAMS']._serialized_start=647 + _globals['_NEWUSERMINIMUMPARAMS']._serialized_end=842 + _globals['_PRELOGINREQUEST']._serialized_start=845 + _globals['_PRELOGINREQUEST']._serialized_end=982 + _globals['_LOGINREQUEST']._serialized_start=985 + _globals['_LOGINREQUEST']._serialized_end=1241 + _globals['_DEVICERESPONSE']._serialized_start=1243 + _globals['_DEVICERESPONSE']._serialized_end=1335 + _globals['_SALT']._serialized_start=1337 + _globals['_SALT']._serialized_end=1423 + _globals['_TWOFACTORCHANNEL']._serialized_start=1425 + _globals['_TWOFACTORCHANNEL']._serialized_end=1457 + _globals['_STARTLOGINREQUEST']._serialized_start=1460 + _globals['_STARTLOGINREQUEST']._serialized_end=1840 + _globals['_LOGINRESPONSE']._serialized_start=1843 + _globals['_LOGINRESPONSE']._serialized_end=2394 + _globals['_SWITCHLISTELEMENT']._serialized_start=2396 + _globals['_SWITCHLISTELEMENT']._serialized_end=2491 + _globals['_SWITCHLISTRESPONSE']._serialized_start=2493 + _globals['_SWITCHLISTRESPONSE']._serialized_end=2566 + _globals['_SSOUSERINFO']._serialized_start=2569 + _globals['_SSOUSERINFO']._serialized_end=2709 + _globals['_PRELOGINRESPONSE']._serialized_start=2712 + _globals['_PRELOGINRESPONSE']._serialized_end=2926 + _globals['_LOGINASUSERREQUEST']._serialized_start=2928 + _globals['_LOGINASUSERREQUEST']._serialized_end=2966 + _globals['_LOGINASUSERRESPONSE']._serialized_start=2968 + _globals['_LOGINASUSERRESPONSE']._serialized_end=3055 + _globals['_VALIDATEAUTHHASHREQUEST']._serialized_start=3058 + _globals['_VALIDATEAUTHHASHREQUEST']._serialized_end=3190 + _globals['_TWOFACTORCHANNELINFO']._serialized_start=3193 + _globals['_TWOFACTORCHANNELINFO']._serialized_end=3517 + _globals['_TWOFACTORDUOSTATUS']._serialized_start=3519 + _globals['_TWOFACTORDUOSTATUS']._serialized_end=3619 + _globals['_TWOFACTORADDREQUEST']._serialized_start=3622 + _globals['_TWOFACTORADDREQUEST']._serialized_end=3821 + _globals['_TWOFACTORRENAMEREQUEST']._serialized_start=3823 + _globals['_TWOFACTORRENAMEREQUEST']._serialized_end=3889 + _globals['_TWOFACTORADDRESPONSE']._serialized_start=3891 + _globals['_TWOFACTORADDRESPONSE']._serialized_end=3952 + _globals['_TWOFACTORDELETEREQUEST']._serialized_start=3954 + _globals['_TWOFACTORDELETEREQUEST']._serialized_end=3999 + _globals['_TWOFACTORLISTRESPONSE']._serialized_start=4001 + _globals['_TWOFACTORLISTRESPONSE']._serialized_end=4098 + _globals['_TWOFACTORUPDATEEXPIRATIONREQUEST']._serialized_start=4100 + _globals['_TWOFACTORUPDATEEXPIRATIONREQUEST']._serialized_end=4189 + _globals['_TWOFACTORVALIDATEREQUEST']._serialized_start=4192 + _globals['_TWOFACTORVALIDATEREQUEST']._serialized_end=4393 + _globals['_TWOFACTORVALIDATERESPONSE']._serialized_start=4395 + _globals['_TWOFACTORVALIDATERESPONSE']._serialized_end=4451 + _globals['_TWOFACTORSENDPUSHREQUEST']._serialized_start=4454 + _globals['_TWOFACTORSENDPUSHREQUEST']._serialized_end=4638 + _globals['_LICENSE']._serialized_start=4641 + _globals['_LICENSE']._serialized_end=4772 + _globals['_OWNERLESSRECORD']._serialized_start=4774 + _globals['_OWNERLESSRECORD']._serialized_end=4845 + _globals['_OWNERLESSRECORDS']._serialized_start=4847 + _globals['_OWNERLESSRECORDS']._serialized_end=4923 + _globals['_USERAUTHREQUEST']._serialized_start=4926 + _globals['_USERAUTHREQUEST']._serialized_end=5141 + _globals['_UIDREQUEST']._serialized_start=5143 + _globals['_UIDREQUEST']._serialized_end=5168 + _globals['_DEVICEUPDATEREQUEST']._serialized_start=5171 + _globals['_DEVICEUPDATEREQUEST']._serialized_end=5426 + _globals['_DEVICEUPDATERESPONSE']._serialized_start=5429 + _globals['_DEVICEUPDATERESPONSE']._serialized_end=5685 + _globals['_REGISTERDEVICEINREGIONREQUEST']._serialized_start=5688 + _globals['_REGISTERDEVICEINREGIONREQUEST']._serialized_end=5901 + _globals['_REGISTRATIONREQUEST']._serialized_start=5904 + _globals['_REGISTRATIONREQUEST']._serialized_end=6280 + _globals['_CONVERTUSERTOV3REQUEST']._serialized_start=6283 + _globals['_CONVERTUSERTOV3REQUEST']._serialized_end=6491 + _globals['_REVISIONRESPONSE']._serialized_start=6493 + _globals['_REVISIONRESPONSE']._serialized_end=6529 + _globals['_CHANGEEMAILREQUEST']._serialized_start=6531 + _globals['_CHANGEEMAILREQUEST']._serialized_end=6569 + _globals['_CHANGEEMAILRESPONSE']._serialized_start=6571 + _globals['_CHANGEEMAILRESPONSE']._serialized_end=6627 + _globals['_EMAILVERIFICATIONLINKRESPONSE']._serialized_start=6629 + _globals['_EMAILVERIFICATIONLINKRESPONSE']._serialized_end=6683 + _globals['_SECURITYDATA']._serialized_start=6685 + _globals['_SECURITYDATA']._serialized_end=6726 + _globals['_SECURITYSCOREDATA']._serialized_start=6728 + _globals['_SECURITYSCOREDATA']._serialized_end=6792 + _globals['_SECURITYDATAREQUEST']._serialized_start=6795 + _globals['_SECURITYDATAREQUEST']._serialized_end=7062 + _globals['_SECURITYREPORTINCREMENTALDATA']._serialized_start=7065 + _globals['_SECURITYREPORTINCREMENTALDATA']._serialized_end=7391 + _globals['_SECURITYREPORT']._serialized_start=7394 + _globals['_SECURITYREPORT']._serialized_end=7681 + _globals['_SECURITYREPORTSAVEREQUEST']._serialized_start=7683 + _globals['_SECURITYREPORTSAVEREQUEST']._serialized_end=7793 + _globals['_SECURITYREPORTREQUEST']._serialized_start=7795 + _globals['_SECURITYREPORTREQUEST']._serialized_end=7836 + _globals['_SECURITYREPORTRESPONSE']._serialized_start=7839 + _globals['_SECURITYREPORTRESPONSE']._serialized_end=8084 + _globals['_INCREMENTALSECURITYDATAREQUEST']._serialized_start=8086 + _globals['_INCREMENTALSECURITYDATAREQUEST']._serialized_end=8145 + _globals['_INCREMENTALSECURITYDATARESPONSE']._serialized_start=8148 + _globals['_INCREMENTALSECURITYDATARESPONSE']._serialized_end=8294 + _globals['_REUSEDPASSWORDSREQUEST']._serialized_start=8296 + _globals['_REUSEDPASSWORDSREQUEST']._serialized_end=8335 + _globals['_SUMMARYCONSOLEREPORT']._serialized_start=8337 + _globals['_SUMMARYCONSOLEREPORT']._serialized_end=8399 + _globals['_CHANGETOKEYTYPEONE']._serialized_start=8401 + _globals['_CHANGETOKEYTYPEONE']._serialized_end=8525 + _globals['_CHANGETOKEYTYPEONEREQUEST']._serialized_start=8527 + _globals['_CHANGETOKEYTYPEONEREQUEST']._serialized_end=8618 + _globals['_CHANGETOKEYTYPEONESTATUS']._serialized_start=8620 + _globals['_CHANGETOKEYTYPEONESTATUS']._serialized_end=8705 + _globals['_CHANGETOKEYTYPEONERESPONSE']._serialized_start=8707 + _globals['_CHANGETOKEYTYPEONERESPONSE']._serialized_end=8811 + _globals['_GETCHANGEKEYTYPESREQUEST']._serialized_start=8814 + _globals['_GETCHANGEKEYTYPESREQUEST']._serialized_end=8999 + _globals['_GETCHANGEKEYTYPESRESPONSE']._serialized_start=9002 + _globals['_GETCHANGEKEYTYPESRESPONSE']._serialized_end=9132 + _globals['_ALLOWEDKEYTYPES']._serialized_start=9135 + _globals['_ALLOWEDKEYTYPES']._serialized_end=9264 + _globals['_CHANGEKEYTYPES']._serialized_start=9266 + _globals['_CHANGEKEYTYPES']._serialized_end=9327 + _globals['_CHANGEKEYTYPE']._serialized_start=9330 + _globals['_CHANGEKEYTYPE']._serialized_end=9544 + _globals['_SETKEY']._serialized_start=9546 + _globals['_SETKEY']._serialized_end=9579 + _globals['_SETKEYREQUEST']._serialized_start=9581 + _globals['_SETKEYREQUEST']._serialized_end=9634 + _globals['_CREATEUSERREQUEST']._serialized_start=9637 + _globals['_CREATEUSERREQUEST']._serialized_end=10295 + _globals['_NODEENFORCEMENTADDORUPDATEREQUEST']._serialized_start=10297 + _globals['_NODEENFORCEMENTADDORUPDATEREQUEST']._serialized_end=10384 + _globals['_NODEENFORCEMENTREMOVEREQUEST']._serialized_start=10386 + _globals['_NODEENFORCEMENTREMOVEREQUEST']._serialized_end=10453 + _globals['_APIREQUESTBYKEY']._serialized_start=10456 + _globals['_APIREQUESTBYKEY']._serialized_end=10615 + _globals['_APIREQUESTBYKATOKAKEY']._serialized_start=10618 + _globals['_APIREQUESTBYKATOKAKEY']._serialized_end=10817 + _globals['_MEMCACHEREQUEST']._serialized_start=10819 + _globals['_MEMCACHEREQUEST']._serialized_end=10865 + _globals['_MEMCACHERESPONSE']._serialized_start=10867 + _globals['_MEMCACHERESPONSE']._serialized_end=10913 + _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_start=10915 + _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_end=11034 + _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_start=11036 + _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_end=11128 + _globals['_DEVICEREGISTRATIONREQUEST']._serialized_start=11131 + _globals['_DEVICEREGISTRATIONREQUEST']._serialized_end=11310 + _globals['_DEVICEVERIFICATIONREQUEST']._serialized_start=11313 + _globals['_DEVICEVERIFICATIONREQUEST']._serialized_end=11467 + _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_start=11470 + _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_end=11648 + _globals['_DEVICEAPPROVALREQUEST']._serialized_start=11651 + _globals['_DEVICEAPPROVALREQUEST']._serialized_end=11851 + _globals['_DEVICEAPPROVALRESPONSE']._serialized_start=11853 + _globals['_DEVICEAPPROVALRESPONSE']._serialized_end=11910 + _globals['_APPROVEDEVICEREQUEST']._serialized_start=11912 + _globals['_APPROVEDEVICEREQUEST']._serialized_end=12038 + _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_start=12040 + _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_end=12109 + _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_start=12111 + _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_end=12200 + _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_start=12202 + _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_end=12321 + _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_start=12323 + _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_end=12395 + _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_start=12397 + _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_end=12491 + _globals['_DEVICE']._serialized_start=12493 + _globals['_DEVICE']._serialized_end=12531 + _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_start=12533 + _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_end=12625 + _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_start=12627 + _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_end=12737 + _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_start=12740 + _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_end=12903 + _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_start=12905 + _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_end=12994 + _globals['_GLOBALUSERACCOUNT']._serialized_start=12996 + _globals['_GLOBALUSERACCOUNT']._serialized_end=13073 + _globals['_ACCOUNTUSERNAME']._serialized_start=13075 + _globals['_ACCOUNTUSERNAME']._serialized_end=13130 + _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_start=13132 + _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_end=13212 + _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_start=13214 + _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_end=13311 + _globals['_USERSETTINGREQUEST']._serialized_start=13313 + _globals['_USERSETTINGREQUEST']._serialized_end=13365 + _globals['_THROTTLESTATE']._serialized_start=13367 + _globals['_THROTTLESTATE']._serialized_end=13469 + _globals['_THROTTLESTATE2']._serialized_start=13472 + _globals['_THROTTLESTATE2']._serialized_end=13653 + _globals['_DEVICEINFORMATION']._serialized_start=13656 + _globals['_DEVICEINFORMATION']._serialized_end=13807 + _globals['_USERSETTING']._serialized_start=13809 + _globals['_USERSETTING']._serialized_end=13851 + _globals['_USERDATAKEYREQUEST']._serialized_start=13853 + _globals['_USERDATAKEYREQUEST']._serialized_end=13899 + _globals['_USERDATAKEYBYNODEREQUEST']._serialized_start=13901 + _globals['_USERDATAKEYBYNODEREQUEST']._serialized_end=13944 + _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_start=13947 + _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_end=14075 + _globals['_USERDATAKEY']._serialized_start=14078 + _globals['_USERDATAKEY']._serialized_end=14227 + _globals['_USERDATAKEYRESPONSE']._serialized_start=14229 + _globals['_USERDATAKEYRESPONSE']._serialized_end=14351 + _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_start=14353 + _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_end=14425 + _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_start=14427 + _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_end=14512 + _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_start=14514 + _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_end=14628 + _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_start=14630 + _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_end=14740 + _globals['_PASSWORDRULES']._serialized_start=14742 + _globals['_PASSWORDRULES']._serialized_end=14860 + _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_start=14863 + _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_end=15192 + _globals['_GETPUBLICKEYSREQUEST']._serialized_start=15194 + _globals['_GETPUBLICKEYSREQUEST']._serialized_end=15235 + _globals['_PUBLICKEYRESPONSE']._serialized_start=15237 + _globals['_PUBLICKEYRESPONSE']._serialized_end=15351 + _globals['_GETPUBLICKEYSRESPONSE']._serialized_start=15353 + _globals['_GETPUBLICKEYSRESPONSE']._serialized_end=15433 + _globals['_SETECCKEYPAIRREQUEST']._serialized_start=15435 + _globals['_SETECCKEYPAIRREQUEST']._serialized_end=15505 + _globals['_SETECCKEYPAIRSREQUEST']._serialized_start=15507 + _globals['_SETECCKEYPAIRSREQUEST']._serialized_end=15580 + _globals['_SETECCKEYPAIRSRESPONSE']._serialized_start=15582 + _globals['_SETECCKEYPAIRSRESPONSE']._serialized_end=15664 + _globals['_TEAMECCKEYPAIR']._serialized_start=15666 + _globals['_TEAMECCKEYPAIR']._serialized_end=15747 + _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_start=15749 + _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_end=15837 + _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_start=15839 + _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_end=15907 + _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_start=15909 + _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_end=15994 + _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_start=15996 + _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_end=16085 + _globals['_ADDAPPSHARESREQUEST']._serialized_start=16087 + _globals['_ADDAPPSHARESREQUEST']._serialized_end=16175 + _globals['_REMOVEAPPSHARESREQUEST']._serialized_start=16177 + _globals['_REMOVEAPPSHARESREQUEST']._serialized_end=16239 + _globals['_APPSHAREADD']._serialized_start=16242 + _globals['_APPSHAREADD']._serialized_end=16377 + _globals['_APPSHARE']._serialized_start=16380 + _globals['_APPSHARE']._serialized_end=16517 + _globals['_ADDAPPCLIENTREQUEST']._serialized_start=16520 + _globals['_ADDAPPCLIENTREQUEST']._serialized_end=16737 + _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_start=16739 + _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_end=16803 + _globals['_ADDEXTERNALSHAREREQUEST']._serialized_start=16806 + _globals['_ADDEXTERNALSHAREREQUEST']._serialized_end=16976 + _globals['_APPCLIENT']._serialized_start=16979 + _globals['_APPCLIENT']._serialized_end=17254 + _globals['_GETAPPINFOREQUEST']._serialized_start=17256 + _globals['_GETAPPINFOREQUEST']._serialized_end=17297 + _globals['_APPINFO']._serialized_start=17300 + _globals['_APPINFO']._serialized_end=17442 + _globals['_GETAPPINFORESPONSE']._serialized_start=17444 + _globals['_GETAPPINFORESPONSE']._serialized_end=17506 + _globals['_APPLICATIONSUMMARY']._serialized_start=17509 + _globals['_APPLICATIONSUMMARY']._serialized_end=17722 + _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_start=17724 + _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_end=17820 + _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_start=17822 + _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_end=17869 + _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_start=17871 + _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_end=17937 + _globals['_SENDSHAREINVITEREQUEST']._serialized_start=17939 + _globals['_SENDSHAREINVITEREQUEST']._serialized_end=17978 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=17981 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=18178 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=18180 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=18235 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=18238 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=18486 + _globals['_REQUESTDOWNLOADREQUEST']._serialized_start=18488 + _globals['_REQUESTDOWNLOADREQUEST']._serialized_end=18531 + _globals['_REQUESTDOWNLOADRESPONSE']._serialized_start=18533 + _globals['_REQUESTDOWNLOADRESPONSE']._serialized_end=18636 + _globals['_DOWNLOAD']._serialized_start=18638 + _globals['_DOWNLOAD']._serialized_end=18706 + _globals['_DELETEUSERREQUEST']._serialized_start=18708 + _globals['_DELETEUSERREQUEST']._serialized_end=18743 + _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_start=18746 + _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_end=18878 + _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_start=18880 + _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_end=18941 + _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_start=18943 + _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_end=19032 + _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_start=19035 + _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_end=19207 + _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_start=19209 + _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_end=19253 + _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_start=19256 + _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_end=19437 + _globals['_USERTEAMKEY']._serialized_start=19440 + _globals['_USERTEAMKEY']._serialized_end=19618 + _globals['_GENERICREQUESTRESPONSE']._serialized_start=19620 + _globals['_GENERICREQUESTRESPONSE']._serialized_end=19661 + _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_start=19663 + _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_end=19765 + _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_start=19767 + _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_end=19847 + _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_start=19850 + _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_end=19982 + _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_start=19985 + _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_end=20292 + _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_start=20295 + _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_end=20434 + _globals['_PASSKEYVALIDATIONREQUEST']._serialized_start=20437 + _globals['_PASSKEYVALIDATIONREQUEST']._serialized_end=20628 + _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_start=20630 + _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_end=20703 + _globals['_UPDATEPASSKEYREQUEST']._serialized_start=20705 + _globals['_UPDATEPASSKEYREQUEST']._serialized_end=20809 + _globals['_PASSKEYLISTREQUEST']._serialized_start=20811 + _globals['_PASSKEYLISTREQUEST']._serialized_end=20856 + _globals['_PASSKEYINFO']._serialized_start=20859 + _globals['_PASSKEYINFO']._serialized_end=21023 + _globals['_PASSKEYLISTRESPONSE']._serialized_start=21025 + _globals['_PASSKEYLISTRESPONSE']._serialized_end=21096 + _globals['_TRANSLATIONINFO']._serialized_start=21098 + _globals['_TRANSLATIONINFO']._serialized_end=21165 + _globals['_TRANSLATIONREQUEST']._serialized_start=21167 + _globals['_TRANSLATIONREQUEST']._serialized_end=21211 + _globals['_TRANSLATIONRESPONSE']._serialized_start=21213 + _globals['_TRANSLATIONRESPONSE']._serialized_end=21292 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi index ab82fcca..13ab5e46 100644 --- a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi @@ -30,6 +30,8 @@ class SupportedLanguage(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): RUSSIAN: _ClassVar[SupportedLanguage] SLOVAK: _ClassVar[SupportedLanguage] SPANISH: _ClassVar[SupportedLanguage] + FINNISH: _ClassVar[SupportedLanguage] + SWEDISH: _ClassVar[SupportedLanguage] class LoginType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -118,6 +120,7 @@ class LoginState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): PASSKEY_INITIATE_CHALLENGE: _ClassVar[LoginState] PASSKEY_AUTH_REQUIRED: _ClassVar[LoginState] PASSKEY_VERIFY_AUTHENTICATION: _ClassVar[LoginState] + AFTER_PASSKEY_LOGIN: _ClassVar[LoginState] LOGGED_IN: _ClassVar[LoginState] class EncryptedDataKeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): @@ -271,6 +274,13 @@ class PasskeyPurpose(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () PK_LOGIN: _ClassVar[PasskeyPurpose] PK_REAUTH: _ClassVar[PasskeyPurpose] + +class ClientFormFactor(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + FF_EMPTY: _ClassVar[ClientFormFactor] + FF_PHONE: _ClassVar[ClientFormFactor] + FF_TABLET: _ClassVar[ClientFormFactor] + FF_WATCH: _ClassVar[ClientFormFactor] ENGLISH: SupportedLanguage ARABIC: SupportedLanguage BRITISH: SupportedLanguage @@ -292,6 +302,8 @@ ROMANIAN: SupportedLanguage RUSSIAN: SupportedLanguage SLOVAK: SupportedLanguage SPANISH: SupportedLanguage +FINNISH: SupportedLanguage +SWEDISH: SupportedLanguage NORMAL: LoginType SSO: LoginType BIO: LoginType @@ -353,6 +365,7 @@ LOGIN_TOKEN_EXPIRED: LoginState PASSKEY_INITIATE_CHALLENGE: LoginState PASSKEY_AUTH_REQUIRED: LoginState PASSKEY_VERIFY_AUTHENTICATION: LoginState +AFTER_PASSKEY_LOGIN: LoginState LOGGED_IN: LoginState NO_KEY: EncryptedDataKeyType BY_DEVICE_PUBLIC_KEY: EncryptedDataKeyType @@ -449,6 +462,10 @@ PLATFORM: AuthenticatorAttachment ALL_SUPPORTED: AuthenticatorAttachment PK_LOGIN: PasskeyPurpose PK_REAUTH: PasskeyPurpose +FF_EMPTY: ClientFormFactor +FF_PHONE: ClientFormFactor +FF_TABLET: ClientFormFactor +FF_WATCH: ClientFormFactor class ApiRequest(_message.Message): __slots__ = ("encryptedTransmissionKey", "publicKeyId", "locale", "encryptedPayload", "encryptionType", "recaptcha", "subEnvironment") @@ -489,12 +506,18 @@ class Transform(_message.Message): def __init__(self, key: _Optional[bytes] = ..., encryptedDeviceToken: _Optional[bytes] = ...) -> None: ... class DeviceRequest(_message.Message): - __slots__ = ("clientVersion", "deviceName") + __slots__ = ("clientVersion", "deviceName", "devicePlatform", "clientFormFactor", "username") CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] DEVICENAME_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] + USERNAME_FIELD_NUMBER: _ClassVar[int] clientVersion: str deviceName: str - def __init__(self, clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ...) -> None: ... + devicePlatform: str + clientFormFactor: ClientFormFactor + username: str + def __init__(self, clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[ClientFormFactor, str]] = ..., username: _Optional[str] = ...) -> None: ... class AuthRequest(_message.Message): __slots__ = ("clientVersion", "username", "encryptedDeviceToken") @@ -581,7 +604,7 @@ class TwoFactorChannel(_message.Message): def __init__(self, type: _Optional[int] = ...) -> None: ... class StartLoginRequest(_message.Message): - __slots__ = ("encryptedDeviceToken", "username", "clientVersion", "messageSessionUid", "encryptedLoginToken", "loginType", "mcEnterpriseId", "loginMethod", "forceNewLogin", "cloneCode", "v2TwoFactorToken", "accountUid") + __slots__ = ("encryptedDeviceToken", "username", "clientVersion", "messageSessionUid", "encryptedLoginToken", "loginType", "mcEnterpriseId", "loginMethod", "forceNewLogin", "cloneCode", "v2TwoFactorToken", "accountUid", "fromSessionToken") ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] USERNAME_FIELD_NUMBER: _ClassVar[int] CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] @@ -594,6 +617,7 @@ class StartLoginRequest(_message.Message): CLONECODE_FIELD_NUMBER: _ClassVar[int] V2TWOFACTORTOKEN_FIELD_NUMBER: _ClassVar[int] ACCOUNTUID_FIELD_NUMBER: _ClassVar[int] + FROMSESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] encryptedDeviceToken: bytes username: str clientVersion: str @@ -606,7 +630,8 @@ class StartLoginRequest(_message.Message): cloneCode: bytes v2TwoFactorToken: str accountUid: bytes - def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., username: _Optional[str] = ..., clientVersion: _Optional[str] = ..., messageSessionUid: _Optional[bytes] = ..., encryptedLoginToken: _Optional[bytes] = ..., loginType: _Optional[_Union[LoginType, str]] = ..., mcEnterpriseId: _Optional[int] = ..., loginMethod: _Optional[_Union[LoginMethod, str]] = ..., forceNewLogin: bool = ..., cloneCode: _Optional[bytes] = ..., v2TwoFactorToken: _Optional[str] = ..., accountUid: _Optional[bytes] = ...) -> None: ... + fromSessionToken: bytes + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., username: _Optional[str] = ..., clientVersion: _Optional[str] = ..., messageSessionUid: _Optional[bytes] = ..., encryptedLoginToken: _Optional[bytes] = ..., loginType: _Optional[_Union[LoginType, str]] = ..., mcEnterpriseId: _Optional[int] = ..., loginMethod: _Optional[_Union[LoginMethod, str]] = ..., forceNewLogin: bool = ..., cloneCode: _Optional[bytes] = ..., v2TwoFactorToken: _Optional[str] = ..., accountUid: _Optional[bytes] = ..., fromSessionToken: _Optional[bytes] = ...) -> None: ... class LoginResponse(_message.Message): __slots__ = ("loginState", "accountUid", "primaryUsername", "encryptedDataKey", "encryptedDataKeyType", "encryptedLoginToken", "encryptedSessionToken", "sessionTokenType", "message", "url", "channels", "salt", "cloneCode", "stateSpecificValue", "ssoClientVersion", "sessionTokenTypeModifier") @@ -644,6 +669,24 @@ class LoginResponse(_message.Message): sessionTokenTypeModifier: str def __init__(self, loginState: _Optional[_Union[LoginState, str]] = ..., accountUid: _Optional[bytes] = ..., primaryUsername: _Optional[str] = ..., encryptedDataKey: _Optional[bytes] = ..., encryptedDataKeyType: _Optional[_Union[EncryptedDataKeyType, str]] = ..., encryptedLoginToken: _Optional[bytes] = ..., encryptedSessionToken: _Optional[bytes] = ..., sessionTokenType: _Optional[_Union[SessionTokenType, str]] = ..., message: _Optional[str] = ..., url: _Optional[str] = ..., channels: _Optional[_Iterable[_Union[TwoFactorChannelInfo, _Mapping]]] = ..., salt: _Optional[_Iterable[_Union[Salt, _Mapping]]] = ..., cloneCode: _Optional[bytes] = ..., stateSpecificValue: _Optional[str] = ..., ssoClientVersion: _Optional[str] = ..., sessionTokenTypeModifier: _Optional[str] = ...) -> None: ... +class SwitchListElement(_message.Message): + __slots__ = ("username", "fullName", "authRequired", "isLinked") + USERNAME_FIELD_NUMBER: _ClassVar[int] + FULLNAME_FIELD_NUMBER: _ClassVar[int] + AUTHREQUIRED_FIELD_NUMBER: _ClassVar[int] + ISLINKED_FIELD_NUMBER: _ClassVar[int] + username: str + fullName: str + authRequired: bool + isLinked: bool + def __init__(self, username: _Optional[str] = ..., fullName: _Optional[str] = ..., authRequired: bool = ..., isLinked: bool = ...) -> None: ... + +class SwitchListResponse(_message.Message): + __slots__ = ("elements",) + ELEMENTS_FIELD_NUMBER: _ClassVar[int] + elements: _containers.RepeatedCompositeFieldContainer[SwitchListElement] + def __init__(self, elements: _Optional[_Iterable[_Union[SwitchListElement, _Mapping]]] = ...) -> None: ... + class SsoUserInfo(_message.Message): __slots__ = ("companyName", "samlRequest", "samlRequestType", "ssoDomainName", "loginUrl", "logoutUrl") COMPANYNAME_FIELD_NUMBER: _ClassVar[int] @@ -871,30 +914,56 @@ class UidRequest(_message.Message): def __init__(self, uid: _Optional[_Iterable[bytes]] = ...) -> None: ... class DeviceUpdateRequest(_message.Message): - __slots__ = ("encryptedDeviceToken", "clientVersion", "deviceName", "devicePublicKey", "deviceStatus") + __slots__ = ("encryptedDeviceToken", "clientVersion", "deviceName", "devicePublicKey", "deviceStatus", "devicePlatform", "clientFormFactor") + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] + DEVICENAME_FIELD_NUMBER: _ClassVar[int] + DEVICEPUBLICKEY_FIELD_NUMBER: _ClassVar[int] + DEVICESTATUS_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] + encryptedDeviceToken: bytes + clientVersion: str + deviceName: str + devicePublicKey: bytes + deviceStatus: DeviceStatus + devicePlatform: str + clientFormFactor: ClientFormFactor + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., deviceStatus: _Optional[_Union[DeviceStatus, str]] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[ClientFormFactor, str]] = ...) -> None: ... + +class DeviceUpdateResponse(_message.Message): + __slots__ = ("encryptedDeviceToken", "clientVersion", "deviceName", "devicePublicKey", "deviceStatus", "devicePlatform", "clientFormFactor") ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] DEVICENAME_FIELD_NUMBER: _ClassVar[int] DEVICEPUBLICKEY_FIELD_NUMBER: _ClassVar[int] DEVICESTATUS_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] encryptedDeviceToken: bytes clientVersion: str deviceName: str devicePublicKey: bytes deviceStatus: DeviceStatus - def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., deviceStatus: _Optional[_Union[DeviceStatus, str]] = ...) -> None: ... + devicePlatform: str + clientFormFactor: ClientFormFactor + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., deviceStatus: _Optional[_Union[DeviceStatus, str]] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[ClientFormFactor, str]] = ...) -> None: ... class RegisterDeviceInRegionRequest(_message.Message): - __slots__ = ("encryptedDeviceToken", "clientVersion", "deviceName", "devicePublicKey") + __slots__ = ("encryptedDeviceToken", "clientVersion", "deviceName", "devicePublicKey", "devicePlatform", "clientFormFactor") ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] DEVICENAME_FIELD_NUMBER: _ClassVar[int] DEVICEPUBLICKEY_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] encryptedDeviceToken: bytes clientVersion: str deviceName: str devicePublicKey: bytes - def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ...) -> None: ... + devicePlatform: str + clientFormFactor: ClientFormFactor + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[ClientFormFactor, str]] = ...) -> None: ... class RegistrationRequest(_message.Message): __slots__ = ("authRequest", "userAuthRequest", "encryptedClientKey", "encryptedPrivateKey", "publicKey", "verificationCode", "deprecatedAuthHashHash", "deprecatedEncryptedClientKey", "deprecatedEncryptedPrivateKey", "deprecatedEncryptionParams") @@ -989,7 +1058,7 @@ class SecurityDataRequest(_message.Message): def __init__(self, recordSecurityData: _Optional[_Iterable[_Union[SecurityData, _Mapping]]] = ..., masterPasswordSecurityData: _Optional[_Iterable[_Union[SecurityData, _Mapping]]] = ..., encryptionType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., recordSecurityScoreData: _Optional[_Iterable[_Union[SecurityScoreData, _Mapping]]] = ...) -> None: ... class SecurityReportIncrementalData(_message.Message): - __slots__ = ("enterpriseUserId", "currentSecurityData", "currentSecurityDataRevision", "oldSecurityData", "oldSecurityDataRevision", "currentDataEncryptionType", "oldDataEncryptionType") + __slots__ = ("enterpriseUserId", "currentSecurityData", "currentSecurityDataRevision", "oldSecurityData", "oldSecurityDataRevision", "currentDataEncryptionType", "oldDataEncryptionType", "recordUid") ENTERPRISEUSERID_FIELD_NUMBER: _ClassVar[int] CURRENTSECURITYDATA_FIELD_NUMBER: _ClassVar[int] CURRENTSECURITYDATAREVISION_FIELD_NUMBER: _ClassVar[int] @@ -997,6 +1066,7 @@ class SecurityReportIncrementalData(_message.Message): OLDSECURITYDATAREVISION_FIELD_NUMBER: _ClassVar[int] CURRENTDATAENCRYPTIONTYPE_FIELD_NUMBER: _ClassVar[int] OLDDATAENCRYPTIONTYPE_FIELD_NUMBER: _ClassVar[int] + RECORDUID_FIELD_NUMBER: _ClassVar[int] enterpriseUserId: int currentSecurityData: bytes currentSecurityDataRevision: int @@ -1004,7 +1074,8 @@ class SecurityReportIncrementalData(_message.Message): oldSecurityDataRevision: int currentDataEncryptionType: _enterprise_pb2.EncryptedKeyType oldDataEncryptionType: _enterprise_pb2.EncryptedKeyType - def __init__(self, enterpriseUserId: _Optional[int] = ..., currentSecurityData: _Optional[bytes] = ..., currentSecurityDataRevision: _Optional[int] = ..., oldSecurityData: _Optional[bytes] = ..., oldSecurityDataRevision: _Optional[int] = ..., currentDataEncryptionType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., oldDataEncryptionType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + recordUid: bytes + def __init__(self, enterpriseUserId: _Optional[int] = ..., currentSecurityData: _Optional[bytes] = ..., currentSecurityDataRevision: _Optional[int] = ..., oldSecurityData: _Optional[bytes] = ..., oldSecurityDataRevision: _Optional[int] = ..., currentDataEncryptionType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., oldDataEncryptionType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., recordUid: _Optional[bytes] = ...) -> None: ... class SecurityReport(_message.Message): __slots__ = ("enterpriseUserId", "encryptedReportData", "revision", "twoFactor", "lastLogin", "numberOfReusedPassword", "securityReportIncrementalData", "userId", "hasOldEncryption") @@ -1029,10 +1100,12 @@ class SecurityReport(_message.Message): def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedReportData: _Optional[bytes] = ..., revision: _Optional[int] = ..., twoFactor: _Optional[str] = ..., lastLogin: _Optional[int] = ..., numberOfReusedPassword: _Optional[int] = ..., securityReportIncrementalData: _Optional[_Iterable[_Union[SecurityReportIncrementalData, _Mapping]]] = ..., userId: _Optional[int] = ..., hasOldEncryption: bool = ...) -> None: ... class SecurityReportSaveRequest(_message.Message): - __slots__ = ("securityReport",) + __slots__ = ("securityReport", "continuationToken") SECURITYREPORT_FIELD_NUMBER: _ClassVar[int] + CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] securityReport: _containers.RepeatedCompositeFieldContainer[SecurityReport] - def __init__(self, securityReport: _Optional[_Iterable[_Union[SecurityReport, _Mapping]]] = ...) -> None: ... + continuationToken: bytes + def __init__(self, securityReport: _Optional[_Iterable[_Union[SecurityReport, _Mapping]]] = ..., continuationToken: _Optional[bytes] = ...) -> None: ... class SecurityReportRequest(_message.Message): __slots__ = ("fromPage",) @@ -1041,7 +1114,7 @@ class SecurityReportRequest(_message.Message): def __init__(self, fromPage: _Optional[int] = ...) -> None: ... class SecurityReportResponse(_message.Message): - __slots__ = ("enterprisePrivateKey", "securityReport", "asOfRevision", "fromPage", "toPage", "complete", "enterpriseEccPrivateKey") + __slots__ = ("enterprisePrivateKey", "securityReport", "asOfRevision", "fromPage", "toPage", "complete", "enterpriseEccPrivateKey", "hasIncrementalData") ENTERPRISEPRIVATEKEY_FIELD_NUMBER: _ClassVar[int] SECURITYREPORT_FIELD_NUMBER: _ClassVar[int] ASOFREVISION_FIELD_NUMBER: _ClassVar[int] @@ -1049,6 +1122,7 @@ class SecurityReportResponse(_message.Message): TOPAGE_FIELD_NUMBER: _ClassVar[int] COMPLETE_FIELD_NUMBER: _ClassVar[int] ENTERPRISEECCPRIVATEKEY_FIELD_NUMBER: _ClassVar[int] + HASINCREMENTALDATA_FIELD_NUMBER: _ClassVar[int] enterprisePrivateKey: bytes securityReport: _containers.RepeatedCompositeFieldContainer[SecurityReport] asOfRevision: int @@ -1056,7 +1130,22 @@ class SecurityReportResponse(_message.Message): toPage: int complete: bool enterpriseEccPrivateKey: bytes - def __init__(self, enterprisePrivateKey: _Optional[bytes] = ..., securityReport: _Optional[_Iterable[_Union[SecurityReport, _Mapping]]] = ..., asOfRevision: _Optional[int] = ..., fromPage: _Optional[int] = ..., toPage: _Optional[int] = ..., complete: bool = ..., enterpriseEccPrivateKey: _Optional[bytes] = ...) -> None: ... + hasIncrementalData: bool + def __init__(self, enterprisePrivateKey: _Optional[bytes] = ..., securityReport: _Optional[_Iterable[_Union[SecurityReport, _Mapping]]] = ..., asOfRevision: _Optional[int] = ..., fromPage: _Optional[int] = ..., toPage: _Optional[int] = ..., complete: bool = ..., enterpriseEccPrivateKey: _Optional[bytes] = ..., hasIncrementalData: bool = ...) -> None: ... + +class IncrementalSecurityDataRequest(_message.Message): + __slots__ = ("continuationToken",) + CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] + continuationToken: bytes + def __init__(self, continuationToken: _Optional[bytes] = ...) -> None: ... + +class IncrementalSecurityDataResponse(_message.Message): + __slots__ = ("securityReportIncrementalData", "continuationToken") + SECURITYREPORTINCREMENTALDATA_FIELD_NUMBER: _ClassVar[int] + CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] + securityReportIncrementalData: _containers.RepeatedCompositeFieldContainer[SecurityReportIncrementalData] + continuationToken: bytes + def __init__(self, securityReportIncrementalData: _Optional[_Iterable[_Union[SecurityReportIncrementalData, _Mapping]]] = ..., continuationToken: _Optional[bytes] = ...) -> None: ... class ReusedPasswordsRequest(_message.Message): __slots__ = ("count",) @@ -1305,14 +1394,18 @@ class MasterPasswordReentryResponse(_message.Message): def __init__(self, status: _Optional[_Union[MasterPasswordReentryStatus, str]] = ...) -> None: ... class DeviceRegistrationRequest(_message.Message): - __slots__ = ("clientVersion", "deviceName", "devicePublicKey") + __slots__ = ("clientVersion", "deviceName", "devicePublicKey", "devicePlatform", "clientFormFactor") CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] DEVICENAME_FIELD_NUMBER: _ClassVar[int] DEVICEPUBLICKEY_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] clientVersion: str deviceName: str devicePublicKey: bytes - def __init__(self, clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ...) -> None: ... + devicePlatform: str + clientFormFactor: ClientFormFactor + def __init__(self, clientVersion: _Optional[str] = ..., deviceName: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[ClientFormFactor, str]] = ...) -> None: ... class DeviceVerificationRequest(_message.Message): __slots__ = ("encryptedDeviceToken", "username", "verificationChannel", "messageSessionUid", "clientVersion") @@ -1847,23 +1940,25 @@ class RemoveAppClientsRequest(_message.Message): def __init__(self, appRecordUid: _Optional[bytes] = ..., clients: _Optional[_Iterable[bytes]] = ...) -> None: ... class AddExternalShareRequest(_message.Message): - __slots__ = ("recordUid", "encryptedRecordKey", "clientId", "accessExpireOn", "id", "isSelfDestruct") + __slots__ = ("recordUid", "encryptedRecordKey", "clientId", "accessExpireOn", "id", "isSelfDestruct", "isEditable") RECORDUID_FIELD_NUMBER: _ClassVar[int] ENCRYPTEDRECORDKEY_FIELD_NUMBER: _ClassVar[int] CLIENTID_FIELD_NUMBER: _ClassVar[int] ACCESSEXPIREON_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] ISSELFDESTRUCT_FIELD_NUMBER: _ClassVar[int] + ISEDITABLE_FIELD_NUMBER: _ClassVar[int] recordUid: bytes encryptedRecordKey: bytes clientId: bytes accessExpireOn: int id: str isSelfDestruct: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedRecordKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., isSelfDestruct: bool = ...) -> None: ... + isEditable: bool + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedRecordKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., isSelfDestruct: bool = ..., isEditable: bool = ...) -> None: ... class AppClient(_message.Message): - __slots__ = ("id", "clientId", "createdOn", "firstAccess", "lastAccess", "publicKey", "lockIp", "ipAddress", "firstAccessExpireOn", "accessExpireOn", "appClientType") + __slots__ = ("id", "clientId", "createdOn", "firstAccess", "lastAccess", "publicKey", "lockIp", "ipAddress", "firstAccessExpireOn", "accessExpireOn", "appClientType", "canEdit") ID_FIELD_NUMBER: _ClassVar[int] CLIENTID_FIELD_NUMBER: _ClassVar[int] CREATEDON_FIELD_NUMBER: _ClassVar[int] @@ -1875,6 +1970,7 @@ class AppClient(_message.Message): FIRSTACCESSEXPIREON_FIELD_NUMBER: _ClassVar[int] ACCESSEXPIREON_FIELD_NUMBER: _ClassVar[int] APPCLIENTTYPE_FIELD_NUMBER: _ClassVar[int] + CANEDIT_FIELD_NUMBER: _ClassVar[int] id: str clientId: bytes createdOn: int @@ -1886,7 +1982,8 @@ class AppClient(_message.Message): firstAccessExpireOn: int accessExpireOn: int appClientType: _enterprise_pb2.AppClientType - def __init__(self, id: _Optional[str] = ..., clientId: _Optional[bytes] = ..., createdOn: _Optional[int] = ..., firstAccess: _Optional[int] = ..., lastAccess: _Optional[int] = ..., publicKey: _Optional[bytes] = ..., lockIp: bool = ..., ipAddress: _Optional[str] = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ...) -> None: ... + canEdit: bool + def __init__(self, id: _Optional[str] = ..., clientId: _Optional[bytes] = ..., createdOn: _Optional[int] = ..., firstAccess: _Optional[int] = ..., lastAccess: _Optional[int] = ..., publicKey: _Optional[bytes] = ..., lockIp: bool = ..., ipAddress: _Optional[str] = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., canEdit: bool = ...) -> None: ... class GetAppInfoRequest(_message.Message): __slots__ = ("appRecordUid",) @@ -2131,14 +2228,20 @@ class PasskeyRegistrationFinalization(_message.Message): def __init__(self, challengeToken: _Optional[bytes] = ..., authenticatorResponse: _Optional[str] = ..., friendlyName: _Optional[str] = ...) -> None: ... class PasskeyAuthenticationRequest(_message.Message): - __slots__ = ("authenticatorAttachment", "passkeyPurpose", "encryptedLoginToken") + __slots__ = ("authenticatorAttachment", "passkeyPurpose", "clientVersion", "encryptedDeviceToken", "username", "encryptedLoginToken") AUTHENTICATORATTACHMENT_FIELD_NUMBER: _ClassVar[int] PASSKEYPURPOSE_FIELD_NUMBER: _ClassVar[int] + CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + USERNAME_FIELD_NUMBER: _ClassVar[int] ENCRYPTEDLOGINTOKEN_FIELD_NUMBER: _ClassVar[int] authenticatorAttachment: AuthenticatorAttachment passkeyPurpose: PasskeyPurpose + clientVersion: str + encryptedDeviceToken: bytes + username: str encryptedLoginToken: bytes - def __init__(self, authenticatorAttachment: _Optional[_Union[AuthenticatorAttachment, str]] = ..., passkeyPurpose: _Optional[_Union[PasskeyPurpose, str]] = ..., encryptedLoginToken: _Optional[bytes] = ...) -> None: ... + def __init__(self, authenticatorAttachment: _Optional[_Union[AuthenticatorAttachment, str]] = ..., passkeyPurpose: _Optional[_Union[PasskeyPurpose, str]] = ..., clientVersion: _Optional[str] = ..., encryptedDeviceToken: _Optional[bytes] = ..., username: _Optional[str] = ..., encryptedLoginToken: _Optional[bytes] = ...) -> None: ... class PasskeyAuthenticationResponse(_message.Message): __slots__ = ("pkRequestOptions", "challengeToken", "encryptedLoginToken") @@ -2209,3 +2312,23 @@ class PasskeyListResponse(_message.Message): PASSKEYINFO_FIELD_NUMBER: _ClassVar[int] passkeyInfo: _containers.RepeatedCompositeFieldContainer[PasskeyInfo] def __init__(self, passkeyInfo: _Optional[_Iterable[_Union[PasskeyInfo, _Mapping]]] = ...) -> None: ... + +class TranslationInfo(_message.Message): + __slots__ = ("translationKey", "translationValue") + TRANSLATIONKEY_FIELD_NUMBER: _ClassVar[int] + TRANSLATIONVALUE_FIELD_NUMBER: _ClassVar[int] + translationKey: str + translationValue: str + def __init__(self, translationKey: _Optional[str] = ..., translationValue: _Optional[str] = ...) -> None: ... + +class TranslationRequest(_message.Message): + __slots__ = ("translationKey",) + TRANSLATIONKEY_FIELD_NUMBER: _ClassVar[int] + translationKey: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, translationKey: _Optional[_Iterable[str]] = ...) -> None: ... + +class TranslationResponse(_message.Message): + __slots__ = ("translationInfo",) + TRANSLATIONINFO_FIELD_NUMBER: _ClassVar[int] + translationInfo: _containers.RepeatedCompositeFieldContainer[TranslationInfo] + def __init__(self, translationInfo: _Optional[_Iterable[_Union[TranslationInfo, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py index b7614a56..d7f840ed 100644 --- a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: AccountSummary.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'AccountSummary.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -25,7 +17,7 @@ from . import APIRequest_pb2 as APIRequest__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x41\x63\x63ountSummary.proto\x12\x0e\x41\x63\x63ountSummary\x1a\x10\x41PIRequest.proto\"N\n\x15\x41\x63\x63ountSummaryRequest\x12\x16\n\x0esummaryVersion\x18\x01 \x01(\x05\x12\x1d\n\x15includeRecentActivity\x18\x02 \x01(\x08\"\x98\x05\n\x16\x41\x63\x63ountSummaryElements\x12\x11\n\tclientKey\x18\x01 \x01(\x0c\x12*\n\x08settings\x18\x02 \x01(\x0b\x32\x18.AccountSummary.Settings\x12*\n\x08keysInfo\x18\x03 \x01(\x0b\x32\x18.AccountSummary.KeysInfo\x12)\n\x08syncLogs\x18\x04 \x03(\x0b\x32\x17.AccountSummary.SyncLog\x12\x19\n\x11isEnterpriseAdmin\x18\x05 \x01(\x08\x12(\n\x07license\x18\x06 \x01(\x0b\x32\x17.AccountSummary.License\x12$\n\x05group\x18\x07 \x01(\x0b\x32\x15.AccountSummary.Group\x12\x32\n\x0c\x45nforcements\x18\x08 \x01(\x0b\x32\x1c.AccountSummary.Enforcements\x12(\n\x06Images\x18\t \x03(\x0b\x32\x18.AccountSummary.KeyValue\x12\x30\n\x0fpersonalLicense\x18\n \x01(\x0b\x32\x17.AccountSummary.License\x12\x1e\n\x16\x66ixSharedFolderRecords\x18\x0b \x01(\x08\x12\x11\n\tusernames\x18\x0c \x03(\t\x12+\n\x07\x64\x65vices\x18\r \x03(\x0b\x32\x1a.AccountSummary.DeviceInfo\x12\x14\n\x0cisShareAdmin\x18\x0e \x01(\x08\x12\x17\n\x0f\x61\x63\x63ountRecovery\x18\x0f \x01(\x08\x12\x1d\n\x15\x61\x63\x63ountRecoveryPrompt\x18\x10 \x01(\x08\x12\'\n\x1fminMasterPasswordLengthNoPrompt\x18\x11 \x01(\x05\x12\x16\n\x0e\x66orbidKeyType2\x18\x12 \x01(\x08\"\xb7\x02\n\nDeviceInfo\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x32\n\x0c\x64\x65viceStatus\x18\x03 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12 \n\x18\x65ncryptedDataKeyDoNotUse\x18\x05 \x01(\x0c\x12\x15\n\rclientVersion\x18\x06 \x01(\t\x12\x10\n\x08username\x18\x07 \x01(\t\x12\x11\n\tipAddress\x18\x08 \x01(\t\x12\x1a\n\x12\x61pproveRequestTime\x18\t \x01(\x03\x12\x1f\n\x17\x65ncryptedDataKeyPresent\x18\n \x01(\x08\x12\x0f\n\x07groupId\x18\x0b \x01(\x03\"\xc1\x01\n\x08KeysInfo\x12\x18\n\x10\x65ncryptionParams\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedDataKey\x18\x02 \x01(\x0c\x12\x19\n\x11\x64\x61taKeyBackupDate\x18\x03 \x01(\x01\x12\x13\n\x0buserAuthUid\x18\x04 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x05 \x01(\x0c\x12\x1e\n\x16\x65ncryptedEccPrivateKey\x18\x06 \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\x07 \x01(\x0c\"\x81\x01\n\x07SyncLog\x12\x13\n\x0b\x63ountryName\x18\x01 \x01(\t\x12\x12\n\nsecondsAgo\x18\x02 \x01(\x03\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x13\n\x0b\x63ountryCode\x18\x04 \x01(\t\x12\x11\n\tdeviceUID\x18\x05 \x01(\x0c\x12\x11\n\tipAddress\x18\x06 \x01(\t\"\xd1\x06\n\x07License\x12\x18\n\x10subscriptionCode\x18\x01 \x01(\t\x12\x15\n\rproductTypeId\x18\x02 \x01(\x05\x12\x17\n\x0fproductTypeName\x18\x03 \x01(\t\x12\x16\n\x0e\x65xpirationDate\x18\x04 \x01(\t\x12\x1e\n\x16secondsUntilExpiration\x18\x05 \x01(\x03\x12\x12\n\nmaxDevices\x18\x06 \x01(\x05\x12\x14\n\x0c\x66ilePlanType\x18\x07 \x01(\x05\x12\x11\n\tbytesUsed\x18\x08 \x01(\x03\x12\x12\n\nbytesTotal\x18\t \x01(\x03\x12%\n\x1dsecondsUntilStorageExpiration\x18\n \x01(\x03\x12\x1d\n\x15storageExpirationDate\x18\x0b \x01(\t\x12,\n$hasAutoRenewableAppstoreSubscription\x18\x0c \x01(\x08\x12\x13\n\x0b\x61\x63\x63ountType\x18\r \x01(\x05\x12\x18\n\x10uploadsRemaining\x18\x0e \x01(\x05\x12\x14\n\x0c\x65nterpriseId\x18\x0f \x01(\x05\x12\x13\n\x0b\x63hatEnabled\x18\x10 \x01(\x08\x12 \n\x18\x61uditAndReportingEnabled\x18\x11 \x01(\x08\x12!\n\x19\x62reachWatchFeatureDisable\x18\x12 \x01(\x08\x12\x12\n\naccountUid\x18\x13 \x01(\x0c\x12\x1c\n\x14\x61llowPersonalLicense\x18\x14 \x01(\x08\x12\x12\n\nlicensedBy\x18\x15 \x01(\t\x12\r\n\x05\x65mail\x18\x16 \x01(\t\x12\x1a\n\x12\x62reachWatchEnabled\x18\x17 \x01(\x08\x12\x1a\n\x12\x62reachWatchScanned\x18\x18 \x01(\x08\x12\x1d\n\x15\x62reachWatchExpiration\x18\x19 \x01(\x03\x12\x1e\n\x16\x62reachWatchDateCreated\x18\x1a \x01(\x03\x12%\n\x05\x65rror\x18\x1b \x01(\x0b\x32\x16.AccountSummary.Result\x12\x12\n\nexpiration\x18\x1d \x01(\x03\x12\x19\n\x11storageExpiration\x18\x1e \x01(\x03\x12\x14\n\x0cuploadsCount\x18\x1f \x01(\x05\x12\r\n\x05units\x18 \x01(\x05\x12\x19\n\x11pendingEnterprise\x18! \x01(\x08\"\xa3\x01\n\x05\x41\x64\x64On\x12\x14\n\x0clicenseKeyId\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x16\n\x0e\x65xpirationDate\x18\x03 \x01(\x03\x12\x13\n\x0b\x63reatedDate\x18\x04 \x01(\x03\x12\x0f\n\x07isTrial\x18\x05 \x01(\x08\x12\x0f\n\x07\x65nabled\x18\x06 \x01(\x08\x12\x0f\n\x07scanned\x18\x07 \x01(\x08\x12\x16\n\x0e\x66\x65\x61tureDisable\x18\x08 \x01(\x08\"\xa2\t\n\x08Settings\x12\r\n\x05\x61udit\x18\x01 \x01(\x08\x12!\n\x19mustPerformAccountShareBy\x18\x02 \x01(\x03\x12>\n\x0eshareAccountTo\x18\x03 \x03(\x0b\x32&.AccountSummary.MissingAccountShareKey\x12+\n\x05rules\x18\x04 \x03(\x0b\x32\x1c.AccountSummary.PasswordRule\x12\x1a\n\x12passwordRulesIntro\x18\x05 \x01(\t\x12\x16\n\x0e\x61utoBackupDays\x18\x06 \x01(\x05\x12\r\n\x05theme\x18\x07 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x08 \x01(\t\x12\x14\n\x0c\x63hannelValue\x18\t \x01(\t\x12\x15\n\rrsaConfigured\x18\n \x01(\x08\x12\x15\n\remailVerified\x18\x0b \x01(\x08\x12\"\n\x1amasterPasswordLastModified\x18\x0c \x01(\x01\x12\x18\n\x10\x61\x63\x63ountFolderKey\x18\r \x01(\x0c\x12\x31\n\x0csecurityKeys\x18\x0e \x03(\x0b\x32\x1b.AccountSummary.SecurityKey\x12+\n\tkeyValues\x18\x0f \x03(\x0b\x32\x18.AccountSummary.KeyValue\x12\x0f\n\x07ssoUser\x18\x10 \x01(\x08\x12\x18\n\x10onlineAccessOnly\x18\x11 \x01(\x08\x12\x1c\n\x14masterPasswordExpiry\x18\x12 \x01(\x05\x12\x19\n\x11twoFactorRequired\x18\x13 \x01(\x08\x12\x16\n\x0e\x64isallowExport\x18\x14 \x01(\x08\x12\x15\n\rrestrictFiles\x18\x15 \x01(\x08\x12\x1a\n\x12restrictAllSharing\x18\x16 \x01(\x08\x12\x17\n\x0frestrictSharing\x18\x17 \x01(\x08\x12\"\n\x1arestrictSharingIncomingAll\x18\x18 \x01(\x08\x12)\n!restrictSharingIncomingEnterprise\x18\x19 \x01(\x08\x12\x13\n\x0blogoutTimer\x18\x1a \x01(\x03\x12\x17\n\x0fpersistentLogin\x18\x1b \x01(\x08\x12\x1c\n\x14ipDisableAutoApprove\x18\x1c \x01(\x08\x12$\n\x1cshareDataKeyWithEccPublicKey\x18\x1d \x01(\x08\x12\'\n\x1fshareDataKeyWithDevicePublicKey\x18\x1e \x01(\x08\x12\x1a\n\x12RecordTypesCounter\x18\x1f \x01(\x05\x12$\n\x1cRecordTypesEnterpriseCounter\x18 \x01(\x05\x12\x1a\n\x12recordTypesEnabled\x18! \x01(\x08\x12\x1c\n\x14\x63\x61nManageRecordTypes\x18\" \x01(\x08\x12\x1d\n\x15recordTypesPAMCounter\x18# \x01(\x05\x12\x1a\n\x12logoutTimerMinutes\x18$ \x01(\x05\x12 \n\x18securityKeysNoUserVerify\x18% \x01(\x08\x12\x36\n\x08\x63hannels\x18& \x03(\x0e\x32$.Authentication.TwoFactorChannelType\"&\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"-\n\x0fKeyValueBoolean\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08\"*\n\x0cKeyValueLong\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\"=\n\x06Result\x12\x12\n\nresultCode\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\"\xc2\x01\n\x0c\x45nforcements\x12)\n\x07strings\x18\x01 \x03(\x0b\x32\x18.AccountSummary.KeyValue\x12\x31\n\x08\x62ooleans\x18\x02 \x03(\x0b\x32\x1f.AccountSummary.KeyValueBoolean\x12+\n\x05longs\x18\x03 \x03(\x0b\x32\x1c.AccountSummary.KeyValueLong\x12\'\n\x05jsons\x18\x04 \x03(\x0b\x32\x18.AccountSummary.KeyValue\"<\n\x16MissingAccountShareKey\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\"u\n\x0cPasswordRule\x12\x10\n\x08ruleType\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\r\n\x05match\x18\x03 \x01(\x08\x12\x0f\n\x07minimum\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\r\n\x05value\x18\x06 \x01(\t\"\x97\x01\n\x0bSecurityKey\x12\x10\n\x08\x64\x65viceId\x18\x01 \x01(\x03\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x11\n\tdateAdded\x18\x03 \x01(\x03\x12\x0f\n\x07isValid\x18\x04 \x01(\x08\x12>\n\x12\x64\x65viceRegistration\x18\x05 \x01(\x0b\x32\".AccountSummary.DeviceRegistration\"y\n\x12\x44\x65viceRegistration\x12\x11\n\tkeyHandle\x18\x01 \x01(\t\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x17\n\x0f\x61ttestationCert\x18\x03 \x01(\t\x12\x0f\n\x07\x63ounter\x18\x04 \x01(\x03\x12\x13\n\x0b\x63ompromised\x18\x05 \x01(\x08\"k\n\x05Group\x12\r\n\x05\x61\x64min\x18\x01 \x01(\x08\x12\x1d\n\x15groupVerificationCode\x18\x02 \x01(\t\x12\x34\n\radministrator\x18\x04 \x01(\x0b\x32\x1d.AccountSummary.Administrator\"\xc0\x01\n\rAdministrator\x12\x11\n\tfirstName\x18\x01 \x01(\t\x12\x10\n\x08lastName\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x1c\n\x14\x63urrentNumberOfUsers\x18\x04 \x01(\x05\x12\x15\n\rnumberOfUsers\x18\x05 \x01(\x05\x12\x18\n\x10subscriptionCode\x18\x07 \x01(\t\x12\x16\n\x0e\x65xpirationDate\x18\x08 \x01(\t\x12\x14\n\x0cpurchaseDate\x18\t \x01(\tB*\n\x18\x63om.keepersecurity.protoB\x0e\x41\x63\x63ountSummaryb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x41\x63\x63ountSummary.proto\x12\x0e\x41\x63\x63ountSummary\x1a\x10\x41PIRequest.proto\"N\n\x15\x41\x63\x63ountSummaryRequest\x12\x16\n\x0esummaryVersion\x18\x01 \x01(\x05\x12\x1d\n\x15includeRecentActivity\x18\x02 \x01(\x08\"\x98\x05\n\x16\x41\x63\x63ountSummaryElements\x12\x11\n\tclientKey\x18\x01 \x01(\x0c\x12*\n\x08settings\x18\x02 \x01(\x0b\x32\x18.AccountSummary.Settings\x12*\n\x08keysInfo\x18\x03 \x01(\x0b\x32\x18.AccountSummary.KeysInfo\x12)\n\x08syncLogs\x18\x04 \x03(\x0b\x32\x17.AccountSummary.SyncLog\x12\x19\n\x11isEnterpriseAdmin\x18\x05 \x01(\x08\x12(\n\x07license\x18\x06 \x01(\x0b\x32\x17.AccountSummary.License\x12$\n\x05group\x18\x07 \x01(\x0b\x32\x15.AccountSummary.Group\x12\x32\n\x0c\x45nforcements\x18\x08 \x01(\x0b\x32\x1c.AccountSummary.Enforcements\x12(\n\x06Images\x18\t \x03(\x0b\x32\x18.AccountSummary.KeyValue\x12\x30\n\x0fpersonalLicense\x18\n \x01(\x0b\x32\x17.AccountSummary.License\x12\x1e\n\x16\x66ixSharedFolderRecords\x18\x0b \x01(\x08\x12\x11\n\tusernames\x18\x0c \x03(\t\x12+\n\x07\x64\x65vices\x18\r \x03(\x0b\x32\x1a.AccountSummary.DeviceInfo\x12\x14\n\x0cisShareAdmin\x18\x0e \x01(\x08\x12\x17\n\x0f\x61\x63\x63ountRecovery\x18\x0f \x01(\x08\x12\x1d\n\x15\x61\x63\x63ountRecoveryPrompt\x18\x10 \x01(\x08\x12\'\n\x1fminMasterPasswordLengthNoPrompt\x18\x11 \x01(\x05\x12\x16\n\x0e\x66orbidKeyType2\x18\x12 \x01(\x08\"\x8b\x03\n\nDeviceInfo\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x32\n\x0c\x64\x65viceStatus\x18\x03 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12 \n\x18\x65ncryptedDataKeyDoNotUse\x18\x05 \x01(\x0c\x12\x15\n\rclientVersion\x18\x06 \x01(\t\x12\x10\n\x08username\x18\x07 \x01(\t\x12\x11\n\tipAddress\x18\x08 \x01(\t\x12\x1a\n\x12\x61pproveRequestTime\x18\t \x01(\x03\x12\x1f\n\x17\x65ncryptedDataKeyPresent\x18\n \x01(\x08\x12\x0f\n\x07groupId\x18\x0b \x01(\x03\x12\x16\n\x0e\x64\x65vicePlatform\x18\x0c \x01(\t\x12:\n\x10\x63lientFormFactor\x18\r \x01(\x0e\x32 .Authentication.ClientFormFactor\"\xc1\x01\n\x08KeysInfo\x12\x18\n\x10\x65ncryptionParams\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedDataKey\x18\x02 \x01(\x0c\x12\x19\n\x11\x64\x61taKeyBackupDate\x18\x03 \x01(\x01\x12\x13\n\x0buserAuthUid\x18\x04 \x01(\x0c\x12\x1b\n\x13\x65ncryptedPrivateKey\x18\x05 \x01(\x0c\x12\x1e\n\x16\x65ncryptedEccPrivateKey\x18\x06 \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\x07 \x01(\x0c\"\x81\x01\n\x07SyncLog\x12\x13\n\x0b\x63ountryName\x18\x01 \x01(\t\x12\x12\n\nsecondsAgo\x18\x02 \x01(\x03\x12\x12\n\ndeviceName\x18\x03 \x01(\t\x12\x13\n\x0b\x63ountryCode\x18\x04 \x01(\t\x12\x11\n\tdeviceUID\x18\x05 \x01(\x0c\x12\x11\n\tipAddress\x18\x06 \x01(\t\"\xfd\x06\n\x07License\x12\x18\n\x10subscriptionCode\x18\x01 \x01(\t\x12\x15\n\rproductTypeId\x18\x02 \x01(\x05\x12\x17\n\x0fproductTypeName\x18\x03 \x01(\t\x12\x16\n\x0e\x65xpirationDate\x18\x04 \x01(\t\x12\x1e\n\x16secondsUntilExpiration\x18\x05 \x01(\x03\x12\x12\n\nmaxDevices\x18\x06 \x01(\x05\x12\x14\n\x0c\x66ilePlanType\x18\x07 \x01(\x05\x12\x11\n\tbytesUsed\x18\x08 \x01(\x03\x12\x12\n\nbytesTotal\x18\t \x01(\x03\x12%\n\x1dsecondsUntilStorageExpiration\x18\n \x01(\x03\x12\x1d\n\x15storageExpirationDate\x18\x0b \x01(\t\x12,\n$hasAutoRenewableAppstoreSubscription\x18\x0c \x01(\x08\x12\x13\n\x0b\x61\x63\x63ountType\x18\r \x01(\x05\x12\x18\n\x10uploadsRemaining\x18\x0e \x01(\x05\x12\x14\n\x0c\x65nterpriseId\x18\x0f \x01(\x05\x12\x13\n\x0b\x63hatEnabled\x18\x10 \x01(\x08\x12 \n\x18\x61uditAndReportingEnabled\x18\x11 \x01(\x08\x12!\n\x19\x62reachWatchFeatureDisable\x18\x12 \x01(\x08\x12\x12\n\naccountUid\x18\x13 \x01(\x0c\x12\x1c\n\x14\x61llowPersonalLicense\x18\x14 \x01(\x08\x12\x12\n\nlicensedBy\x18\x15 \x01(\t\x12\r\n\x05\x65mail\x18\x16 \x01(\t\x12\x1a\n\x12\x62reachWatchEnabled\x18\x17 \x01(\x08\x12\x1a\n\x12\x62reachWatchScanned\x18\x18 \x01(\x08\x12\x1d\n\x15\x62reachWatchExpiration\x18\x19 \x01(\x03\x12\x1e\n\x16\x62reachWatchDateCreated\x18\x1a \x01(\x03\x12%\n\x05\x65rror\x18\x1b \x01(\x0b\x32\x16.AccountSummary.Result\x12\x12\n\nexpiration\x18\x1d \x01(\x03\x12\x19\n\x11storageExpiration\x18\x1e \x01(\x03\x12\x14\n\x0cuploadsCount\x18\x1f \x01(\x05\x12\r\n\x05units\x18 \x01(\x05\x12\x19\n\x11pendingEnterprise\x18! \x01(\x08\x12\x14\n\x0cisPamEnabled\x18\" \x01(\x08\x12\x14\n\x0cisKsmEnabled\x18# \x01(\x08\"\xa3\x01\n\x05\x41\x64\x64On\x12\x14\n\x0clicenseKeyId\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x16\n\x0e\x65xpirationDate\x18\x03 \x01(\x03\x12\x13\n\x0b\x63reatedDate\x18\x04 \x01(\x03\x12\x0f\n\x07isTrial\x18\x05 \x01(\x08\x12\x0f\n\x07\x65nabled\x18\x06 \x01(\x08\x12\x0f\n\x07scanned\x18\x07 \x01(\x08\x12\x16\n\x0e\x66\x65\x61tureDisable\x18\x08 \x01(\x08\"\xbd\t\n\x08Settings\x12\r\n\x05\x61udit\x18\x01 \x01(\x08\x12!\n\x19mustPerformAccountShareBy\x18\x02 \x01(\x03\x12>\n\x0eshareAccountTo\x18\x03 \x03(\x0b\x32&.AccountSummary.MissingAccountShareKey\x12+\n\x05rules\x18\x04 \x03(\x0b\x32\x1c.AccountSummary.PasswordRule\x12\x1a\n\x12passwordRulesIntro\x18\x05 \x01(\t\x12\x16\n\x0e\x61utoBackupDays\x18\x06 \x01(\x05\x12\r\n\x05theme\x18\x07 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x08 \x01(\t\x12\x14\n\x0c\x63hannelValue\x18\t \x01(\t\x12\x15\n\rrsaConfigured\x18\n \x01(\x08\x12\x15\n\remailVerified\x18\x0b \x01(\x08\x12\"\n\x1amasterPasswordLastModified\x18\x0c \x01(\x01\x12\x18\n\x10\x61\x63\x63ountFolderKey\x18\r \x01(\x0c\x12\x31\n\x0csecurityKeys\x18\x0e \x03(\x0b\x32\x1b.AccountSummary.SecurityKey\x12+\n\tkeyValues\x18\x0f \x03(\x0b\x32\x18.AccountSummary.KeyValue\x12\x0f\n\x07ssoUser\x18\x10 \x01(\x08\x12\x18\n\x10onlineAccessOnly\x18\x11 \x01(\x08\x12\x1c\n\x14masterPasswordExpiry\x18\x12 \x01(\x05\x12\x19\n\x11twoFactorRequired\x18\x13 \x01(\x08\x12\x16\n\x0e\x64isallowExport\x18\x14 \x01(\x08\x12\x15\n\rrestrictFiles\x18\x15 \x01(\x08\x12\x1a\n\x12restrictAllSharing\x18\x16 \x01(\x08\x12\x17\n\x0frestrictSharing\x18\x17 \x01(\x08\x12\"\n\x1arestrictSharingIncomingAll\x18\x18 \x01(\x08\x12)\n!restrictSharingIncomingEnterprise\x18\x19 \x01(\x08\x12\x13\n\x0blogoutTimer\x18\x1a \x01(\x03\x12\x17\n\x0fpersistentLogin\x18\x1b \x01(\x08\x12\x1c\n\x14ipDisableAutoApprove\x18\x1c \x01(\x08\x12$\n\x1cshareDataKeyWithEccPublicKey\x18\x1d \x01(\x08\x12\'\n\x1fshareDataKeyWithDevicePublicKey\x18\x1e \x01(\x08\x12\x1a\n\x12RecordTypesCounter\x18\x1f \x01(\x05\x12$\n\x1cRecordTypesEnterpriseCounter\x18 \x01(\x05\x12\x1a\n\x12recordTypesEnabled\x18! \x01(\x08\x12\x1c\n\x14\x63\x61nManageRecordTypes\x18\" \x01(\x08\x12\x1d\n\x15recordTypesPAMCounter\x18# \x01(\x05\x12\x1a\n\x12logoutTimerMinutes\x18$ \x01(\x05\x12 \n\x18securityKeysNoUserVerify\x18% \x01(\x08\x12\x36\n\x08\x63hannels\x18& \x03(\x0e\x32$.Authentication.TwoFactorChannelType\x12\x19\n\x11personalUsernames\x18\' \x03(\t\"&\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"-\n\x0fKeyValueBoolean\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08\"*\n\x0cKeyValueLong\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\"=\n\x06Result\x12\x12\n\nresultCode\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\"\xc2\x01\n\x0c\x45nforcements\x12)\n\x07strings\x18\x01 \x03(\x0b\x32\x18.AccountSummary.KeyValue\x12\x31\n\x08\x62ooleans\x18\x02 \x03(\x0b\x32\x1f.AccountSummary.KeyValueBoolean\x12+\n\x05longs\x18\x03 \x03(\x0b\x32\x1c.AccountSummary.KeyValueLong\x12\'\n\x05jsons\x18\x04 \x03(\x0b\x32\x18.AccountSummary.KeyValue\"<\n\x16MissingAccountShareKey\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\"u\n\x0cPasswordRule\x12\x10\n\x08ruleType\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\r\n\x05match\x18\x03 \x01(\x08\x12\x0f\n\x07minimum\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\r\n\x05value\x18\x06 \x01(\t\"\x97\x01\n\x0bSecurityKey\x12\x10\n\x08\x64\x65viceId\x18\x01 \x01(\x03\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x11\n\tdateAdded\x18\x03 \x01(\x03\x12\x0f\n\x07isValid\x18\x04 \x01(\x08\x12>\n\x12\x64\x65viceRegistration\x18\x05 \x01(\x0b\x32\".AccountSummary.DeviceRegistration\"y\n\x12\x44\x65viceRegistration\x12\x11\n\tkeyHandle\x18\x01 \x01(\t\x12\x11\n\tpublicKey\x18\x02 \x01(\x0c\x12\x17\n\x0f\x61ttestationCert\x18\x03 \x01(\t\x12\x0f\n\x07\x63ounter\x18\x04 \x01(\x03\x12\x13\n\x0b\x63ompromised\x18\x05 \x01(\x08\"k\n\x05Group\x12\r\n\x05\x61\x64min\x18\x01 \x01(\x08\x12\x1d\n\x15groupVerificationCode\x18\x02 \x01(\t\x12\x34\n\radministrator\x18\x04 \x01(\x0b\x32\x1d.AccountSummary.Administrator\"\xc0\x01\n\rAdministrator\x12\x11\n\tfirstName\x18\x01 \x01(\t\x12\x10\n\x08lastName\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x1c\n\x14\x63urrentNumberOfUsers\x18\x04 \x01(\x05\x12\x15\n\rnumberOfUsers\x18\x05 \x01(\x05\x12\x18\n\x10subscriptionCode\x18\x07 \x01(\t\x12\x16\n\x0e\x65xpirationDate\x18\x08 \x01(\t\x12\x14\n\x0cpurchaseDate\x18\t \x01(\tB*\n\x18\x63om.keepersecurity.protoB\x0e\x41\x63\x63ountSummaryb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -38,37 +30,37 @@ _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_start=139 _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_end=803 _globals['_DEVICEINFO']._serialized_start=806 - _globals['_DEVICEINFO']._serialized_end=1117 - _globals['_KEYSINFO']._serialized_start=1120 - _globals['_KEYSINFO']._serialized_end=1313 - _globals['_SYNCLOG']._serialized_start=1316 - _globals['_SYNCLOG']._serialized_end=1445 - _globals['_LICENSE']._serialized_start=1448 - _globals['_LICENSE']._serialized_end=2297 - _globals['_ADDON']._serialized_start=2300 - _globals['_ADDON']._serialized_end=2463 - _globals['_SETTINGS']._serialized_start=2466 - _globals['_SETTINGS']._serialized_end=3652 - _globals['_KEYVALUE']._serialized_start=3654 - _globals['_KEYVALUE']._serialized_end=3692 - _globals['_KEYVALUEBOOLEAN']._serialized_start=3694 - _globals['_KEYVALUEBOOLEAN']._serialized_end=3739 - _globals['_KEYVALUELONG']._serialized_start=3741 - _globals['_KEYVALUELONG']._serialized_end=3783 - _globals['_RESULT']._serialized_start=3785 - _globals['_RESULT']._serialized_end=3846 - _globals['_ENFORCEMENTS']._serialized_start=3849 - _globals['_ENFORCEMENTS']._serialized_end=4043 - _globals['_MISSINGACCOUNTSHAREKEY']._serialized_start=4045 - _globals['_MISSINGACCOUNTSHAREKEY']._serialized_end=4105 - _globals['_PASSWORDRULE']._serialized_start=4107 - _globals['_PASSWORDRULE']._serialized_end=4224 - _globals['_SECURITYKEY']._serialized_start=4227 - _globals['_SECURITYKEY']._serialized_end=4378 - _globals['_DEVICEREGISTRATION']._serialized_start=4380 - _globals['_DEVICEREGISTRATION']._serialized_end=4501 - _globals['_GROUP']._serialized_start=4503 - _globals['_GROUP']._serialized_end=4610 - _globals['_ADMINISTRATOR']._serialized_start=4613 - _globals['_ADMINISTRATOR']._serialized_end=4805 + _globals['_DEVICEINFO']._serialized_end=1201 + _globals['_KEYSINFO']._serialized_start=1204 + _globals['_KEYSINFO']._serialized_end=1397 + _globals['_SYNCLOG']._serialized_start=1400 + _globals['_SYNCLOG']._serialized_end=1529 + _globals['_LICENSE']._serialized_start=1532 + _globals['_LICENSE']._serialized_end=2425 + _globals['_ADDON']._serialized_start=2428 + _globals['_ADDON']._serialized_end=2591 + _globals['_SETTINGS']._serialized_start=2594 + _globals['_SETTINGS']._serialized_end=3807 + _globals['_KEYVALUE']._serialized_start=3809 + _globals['_KEYVALUE']._serialized_end=3847 + _globals['_KEYVALUEBOOLEAN']._serialized_start=3849 + _globals['_KEYVALUEBOOLEAN']._serialized_end=3894 + _globals['_KEYVALUELONG']._serialized_start=3896 + _globals['_KEYVALUELONG']._serialized_end=3938 + _globals['_RESULT']._serialized_start=3940 + _globals['_RESULT']._serialized_end=4001 + _globals['_ENFORCEMENTS']._serialized_start=4004 + _globals['_ENFORCEMENTS']._serialized_end=4198 + _globals['_MISSINGACCOUNTSHAREKEY']._serialized_start=4200 + _globals['_MISSINGACCOUNTSHAREKEY']._serialized_end=4260 + _globals['_PASSWORDRULE']._serialized_start=4262 + _globals['_PASSWORDRULE']._serialized_end=4379 + _globals['_SECURITYKEY']._serialized_start=4382 + _globals['_SECURITYKEY']._serialized_end=4533 + _globals['_DEVICEREGISTRATION']._serialized_start=4535 + _globals['_DEVICEREGISTRATION']._serialized_end=4656 + _globals['_GROUP']._serialized_start=4658 + _globals['_GROUP']._serialized_end=4765 + _globals['_ADMINISTRATOR']._serialized_start=4768 + _globals['_ADMINISTRATOR']._serialized_end=4960 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi index edeed30c..271a2ad1 100644 --- a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi @@ -55,7 +55,7 @@ class AccountSummaryElements(_message.Message): def __init__(self, clientKey: _Optional[bytes] = ..., settings: _Optional[_Union[Settings, _Mapping]] = ..., keysInfo: _Optional[_Union[KeysInfo, _Mapping]] = ..., syncLogs: _Optional[_Iterable[_Union[SyncLog, _Mapping]]] = ..., isEnterpriseAdmin: bool = ..., license: _Optional[_Union[License, _Mapping]] = ..., group: _Optional[_Union[Group, _Mapping]] = ..., Enforcements: _Optional[_Union[Enforcements, _Mapping]] = ..., Images: _Optional[_Iterable[_Union[KeyValue, _Mapping]]] = ..., personalLicense: _Optional[_Union[License, _Mapping]] = ..., fixSharedFolderRecords: bool = ..., usernames: _Optional[_Iterable[str]] = ..., devices: _Optional[_Iterable[_Union[DeviceInfo, _Mapping]]] = ..., isShareAdmin: bool = ..., accountRecovery: bool = ..., accountRecoveryPrompt: bool = ..., minMasterPasswordLengthNoPrompt: _Optional[int] = ..., forbidKeyType2: bool = ...) -> None: ... class DeviceInfo(_message.Message): - __slots__ = ("encryptedDeviceToken", "deviceName", "deviceStatus", "devicePublicKey", "encryptedDataKeyDoNotUse", "clientVersion", "username", "ipAddress", "approveRequestTime", "encryptedDataKeyPresent", "groupId") + __slots__ = ("encryptedDeviceToken", "deviceName", "deviceStatus", "devicePublicKey", "encryptedDataKeyDoNotUse", "clientVersion", "username", "ipAddress", "approveRequestTime", "encryptedDataKeyPresent", "groupId", "devicePlatform", "clientFormFactor") ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] DEVICENAME_FIELD_NUMBER: _ClassVar[int] DEVICESTATUS_FIELD_NUMBER: _ClassVar[int] @@ -67,6 +67,8 @@ class DeviceInfo(_message.Message): APPROVEREQUESTTIME_FIELD_NUMBER: _ClassVar[int] ENCRYPTEDDATAKEYPRESENT_FIELD_NUMBER: _ClassVar[int] GROUPID_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] encryptedDeviceToken: bytes deviceName: str deviceStatus: _APIRequest_pb2.DeviceStatus @@ -78,7 +80,9 @@ class DeviceInfo(_message.Message): approveRequestTime: int encryptedDataKeyPresent: bool groupId: int - def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., deviceName: _Optional[str] = ..., deviceStatus: _Optional[_Union[_APIRequest_pb2.DeviceStatus, str]] = ..., devicePublicKey: _Optional[bytes] = ..., encryptedDataKeyDoNotUse: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., username: _Optional[str] = ..., ipAddress: _Optional[str] = ..., approveRequestTime: _Optional[int] = ..., encryptedDataKeyPresent: bool = ..., groupId: _Optional[int] = ...) -> None: ... + devicePlatform: str + clientFormFactor: _APIRequest_pb2.ClientFormFactor + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., deviceName: _Optional[str] = ..., deviceStatus: _Optional[_Union[_APIRequest_pb2.DeviceStatus, str]] = ..., devicePublicKey: _Optional[bytes] = ..., encryptedDataKeyDoNotUse: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., username: _Optional[str] = ..., ipAddress: _Optional[str] = ..., approveRequestTime: _Optional[int] = ..., encryptedDataKeyPresent: bool = ..., groupId: _Optional[int] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[_APIRequest_pb2.ClientFormFactor, str]] = ...) -> None: ... class KeysInfo(_message.Message): __slots__ = ("encryptionParams", "encryptedDataKey", "dataKeyBackupDate", "userAuthUid", "encryptedPrivateKey", "encryptedEccPrivateKey", "eccPublicKey") @@ -115,7 +119,7 @@ class SyncLog(_message.Message): def __init__(self, countryName: _Optional[str] = ..., secondsAgo: _Optional[int] = ..., deviceName: _Optional[str] = ..., countryCode: _Optional[str] = ..., deviceUID: _Optional[bytes] = ..., ipAddress: _Optional[str] = ...) -> None: ... class License(_message.Message): - __slots__ = ("subscriptionCode", "productTypeId", "productTypeName", "expirationDate", "secondsUntilExpiration", "maxDevices", "filePlanType", "bytesUsed", "bytesTotal", "secondsUntilStorageExpiration", "storageExpirationDate", "hasAutoRenewableAppstoreSubscription", "accountType", "uploadsRemaining", "enterpriseId", "chatEnabled", "auditAndReportingEnabled", "breachWatchFeatureDisable", "accountUid", "allowPersonalLicense", "licensedBy", "email", "breachWatchEnabled", "breachWatchScanned", "breachWatchExpiration", "breachWatchDateCreated", "error", "expiration", "storageExpiration", "uploadsCount", "units", "pendingEnterprise") + __slots__ = ("subscriptionCode", "productTypeId", "productTypeName", "expirationDate", "secondsUntilExpiration", "maxDevices", "filePlanType", "bytesUsed", "bytesTotal", "secondsUntilStorageExpiration", "storageExpirationDate", "hasAutoRenewableAppstoreSubscription", "accountType", "uploadsRemaining", "enterpriseId", "chatEnabled", "auditAndReportingEnabled", "breachWatchFeatureDisable", "accountUid", "allowPersonalLicense", "licensedBy", "email", "breachWatchEnabled", "breachWatchScanned", "breachWatchExpiration", "breachWatchDateCreated", "error", "expiration", "storageExpiration", "uploadsCount", "units", "pendingEnterprise", "isPamEnabled", "isKsmEnabled") SUBSCRIPTIONCODE_FIELD_NUMBER: _ClassVar[int] PRODUCTTYPEID_FIELD_NUMBER: _ClassVar[int] PRODUCTTYPENAME_FIELD_NUMBER: _ClassVar[int] @@ -148,6 +152,8 @@ class License(_message.Message): UPLOADSCOUNT_FIELD_NUMBER: _ClassVar[int] UNITS_FIELD_NUMBER: _ClassVar[int] PENDINGENTERPRISE_FIELD_NUMBER: _ClassVar[int] + ISPAMENABLED_FIELD_NUMBER: _ClassVar[int] + ISKSMENABLED_FIELD_NUMBER: _ClassVar[int] subscriptionCode: str productTypeId: int productTypeName: str @@ -180,7 +186,9 @@ class License(_message.Message): uploadsCount: int units: int pendingEnterprise: bool - def __init__(self, subscriptionCode: _Optional[str] = ..., productTypeId: _Optional[int] = ..., productTypeName: _Optional[str] = ..., expirationDate: _Optional[str] = ..., secondsUntilExpiration: _Optional[int] = ..., maxDevices: _Optional[int] = ..., filePlanType: _Optional[int] = ..., bytesUsed: _Optional[int] = ..., bytesTotal: _Optional[int] = ..., secondsUntilStorageExpiration: _Optional[int] = ..., storageExpirationDate: _Optional[str] = ..., hasAutoRenewableAppstoreSubscription: bool = ..., accountType: _Optional[int] = ..., uploadsRemaining: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., chatEnabled: bool = ..., auditAndReportingEnabled: bool = ..., breachWatchFeatureDisable: bool = ..., accountUid: _Optional[bytes] = ..., allowPersonalLicense: bool = ..., licensedBy: _Optional[str] = ..., email: _Optional[str] = ..., breachWatchEnabled: bool = ..., breachWatchScanned: bool = ..., breachWatchExpiration: _Optional[int] = ..., breachWatchDateCreated: _Optional[int] = ..., error: _Optional[_Union[Result, _Mapping]] = ..., expiration: _Optional[int] = ..., storageExpiration: _Optional[int] = ..., uploadsCount: _Optional[int] = ..., units: _Optional[int] = ..., pendingEnterprise: bool = ...) -> None: ... + isPamEnabled: bool + isKsmEnabled: bool + def __init__(self, subscriptionCode: _Optional[str] = ..., productTypeId: _Optional[int] = ..., productTypeName: _Optional[str] = ..., expirationDate: _Optional[str] = ..., secondsUntilExpiration: _Optional[int] = ..., maxDevices: _Optional[int] = ..., filePlanType: _Optional[int] = ..., bytesUsed: _Optional[int] = ..., bytesTotal: _Optional[int] = ..., secondsUntilStorageExpiration: _Optional[int] = ..., storageExpirationDate: _Optional[str] = ..., hasAutoRenewableAppstoreSubscription: bool = ..., accountType: _Optional[int] = ..., uploadsRemaining: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., chatEnabled: bool = ..., auditAndReportingEnabled: bool = ..., breachWatchFeatureDisable: bool = ..., accountUid: _Optional[bytes] = ..., allowPersonalLicense: bool = ..., licensedBy: _Optional[str] = ..., email: _Optional[str] = ..., breachWatchEnabled: bool = ..., breachWatchScanned: bool = ..., breachWatchExpiration: _Optional[int] = ..., breachWatchDateCreated: _Optional[int] = ..., error: _Optional[_Union[Result, _Mapping]] = ..., expiration: _Optional[int] = ..., storageExpiration: _Optional[int] = ..., uploadsCount: _Optional[int] = ..., units: _Optional[int] = ..., pendingEnterprise: bool = ..., isPamEnabled: bool = ..., isKsmEnabled: bool = ...) -> None: ... class AddOn(_message.Message): __slots__ = ("licenseKeyId", "name", "expirationDate", "createdDate", "isTrial", "enabled", "scanned", "featureDisable") @@ -203,7 +211,7 @@ class AddOn(_message.Message): def __init__(self, licenseKeyId: _Optional[int] = ..., name: _Optional[str] = ..., expirationDate: _Optional[int] = ..., createdDate: _Optional[int] = ..., isTrial: bool = ..., enabled: bool = ..., scanned: bool = ..., featureDisable: bool = ...) -> None: ... class Settings(_message.Message): - __slots__ = ("audit", "mustPerformAccountShareBy", "shareAccountTo", "rules", "passwordRulesIntro", "autoBackupDays", "theme", "channel", "channelValue", "rsaConfigured", "emailVerified", "masterPasswordLastModified", "accountFolderKey", "securityKeys", "keyValues", "ssoUser", "onlineAccessOnly", "masterPasswordExpiry", "twoFactorRequired", "disallowExport", "restrictFiles", "restrictAllSharing", "restrictSharing", "restrictSharingIncomingAll", "restrictSharingIncomingEnterprise", "logoutTimer", "persistentLogin", "ipDisableAutoApprove", "shareDataKeyWithEccPublicKey", "shareDataKeyWithDevicePublicKey", "RecordTypesCounter", "RecordTypesEnterpriseCounter", "recordTypesEnabled", "canManageRecordTypes", "recordTypesPAMCounter", "logoutTimerMinutes", "securityKeysNoUserVerify", "channels") + __slots__ = ("audit", "mustPerformAccountShareBy", "shareAccountTo", "rules", "passwordRulesIntro", "autoBackupDays", "theme", "channel", "channelValue", "rsaConfigured", "emailVerified", "masterPasswordLastModified", "accountFolderKey", "securityKeys", "keyValues", "ssoUser", "onlineAccessOnly", "masterPasswordExpiry", "twoFactorRequired", "disallowExport", "restrictFiles", "restrictAllSharing", "restrictSharing", "restrictSharingIncomingAll", "restrictSharingIncomingEnterprise", "logoutTimer", "persistentLogin", "ipDisableAutoApprove", "shareDataKeyWithEccPublicKey", "shareDataKeyWithDevicePublicKey", "RecordTypesCounter", "RecordTypesEnterpriseCounter", "recordTypesEnabled", "canManageRecordTypes", "recordTypesPAMCounter", "logoutTimerMinutes", "securityKeysNoUserVerify", "channels", "personalUsernames") AUDIT_FIELD_NUMBER: _ClassVar[int] MUSTPERFORMACCOUNTSHAREBY_FIELD_NUMBER: _ClassVar[int] SHAREACCOUNTTO_FIELD_NUMBER: _ClassVar[int] @@ -242,6 +250,7 @@ class Settings(_message.Message): LOGOUTTIMERMINUTES_FIELD_NUMBER: _ClassVar[int] SECURITYKEYSNOUSERVERIFY_FIELD_NUMBER: _ClassVar[int] CHANNELS_FIELD_NUMBER: _ClassVar[int] + PERSONALUSERNAMES_FIELD_NUMBER: _ClassVar[int] audit: bool mustPerformAccountShareBy: int shareAccountTo: _containers.RepeatedCompositeFieldContainer[MissingAccountShareKey] @@ -280,7 +289,8 @@ class Settings(_message.Message): logoutTimerMinutes: int securityKeysNoUserVerify: bool channels: _containers.RepeatedScalarFieldContainer[_APIRequest_pb2.TwoFactorChannelType] - def __init__(self, audit: bool = ..., mustPerformAccountShareBy: _Optional[int] = ..., shareAccountTo: _Optional[_Iterable[_Union[MissingAccountShareKey, _Mapping]]] = ..., rules: _Optional[_Iterable[_Union[PasswordRule, _Mapping]]] = ..., passwordRulesIntro: _Optional[str] = ..., autoBackupDays: _Optional[int] = ..., theme: _Optional[str] = ..., channel: _Optional[str] = ..., channelValue: _Optional[str] = ..., rsaConfigured: bool = ..., emailVerified: bool = ..., masterPasswordLastModified: _Optional[float] = ..., accountFolderKey: _Optional[bytes] = ..., securityKeys: _Optional[_Iterable[_Union[SecurityKey, _Mapping]]] = ..., keyValues: _Optional[_Iterable[_Union[KeyValue, _Mapping]]] = ..., ssoUser: bool = ..., onlineAccessOnly: bool = ..., masterPasswordExpiry: _Optional[int] = ..., twoFactorRequired: bool = ..., disallowExport: bool = ..., restrictFiles: bool = ..., restrictAllSharing: bool = ..., restrictSharing: bool = ..., restrictSharingIncomingAll: bool = ..., restrictSharingIncomingEnterprise: bool = ..., logoutTimer: _Optional[int] = ..., persistentLogin: bool = ..., ipDisableAutoApprove: bool = ..., shareDataKeyWithEccPublicKey: bool = ..., shareDataKeyWithDevicePublicKey: bool = ..., RecordTypesCounter: _Optional[int] = ..., RecordTypesEnterpriseCounter: _Optional[int] = ..., recordTypesEnabled: bool = ..., canManageRecordTypes: bool = ..., recordTypesPAMCounter: _Optional[int] = ..., logoutTimerMinutes: _Optional[int] = ..., securityKeysNoUserVerify: bool = ..., channels: _Optional[_Iterable[_Union[_APIRequest_pb2.TwoFactorChannelType, str]]] = ...) -> None: ... + personalUsernames: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, audit: bool = ..., mustPerformAccountShareBy: _Optional[int] = ..., shareAccountTo: _Optional[_Iterable[_Union[MissingAccountShareKey, _Mapping]]] = ..., rules: _Optional[_Iterable[_Union[PasswordRule, _Mapping]]] = ..., passwordRulesIntro: _Optional[str] = ..., autoBackupDays: _Optional[int] = ..., theme: _Optional[str] = ..., channel: _Optional[str] = ..., channelValue: _Optional[str] = ..., rsaConfigured: bool = ..., emailVerified: bool = ..., masterPasswordLastModified: _Optional[float] = ..., accountFolderKey: _Optional[bytes] = ..., securityKeys: _Optional[_Iterable[_Union[SecurityKey, _Mapping]]] = ..., keyValues: _Optional[_Iterable[_Union[KeyValue, _Mapping]]] = ..., ssoUser: bool = ..., onlineAccessOnly: bool = ..., masterPasswordExpiry: _Optional[int] = ..., twoFactorRequired: bool = ..., disallowExport: bool = ..., restrictFiles: bool = ..., restrictAllSharing: bool = ..., restrictSharing: bool = ..., restrictSharingIncomingAll: bool = ..., restrictSharingIncomingEnterprise: bool = ..., logoutTimer: _Optional[int] = ..., persistentLogin: bool = ..., ipDisableAutoApprove: bool = ..., shareDataKeyWithEccPublicKey: bool = ..., shareDataKeyWithDevicePublicKey: bool = ..., RecordTypesCounter: _Optional[int] = ..., RecordTypesEnterpriseCounter: _Optional[int] = ..., recordTypesEnabled: bool = ..., canManageRecordTypes: bool = ..., recordTypesPAMCounter: _Optional[int] = ..., logoutTimerMinutes: _Optional[int] = ..., securityKeysNoUserVerify: bool = ..., channels: _Optional[_Iterable[_Union[_APIRequest_pb2.TwoFactorChannelType, str]]] = ..., personalUsernames: _Optional[_Iterable[str]] = ...) -> None: ... class KeyValue(_message.Message): __slots__ = ("key", "value") diff --git a/keepersdk-package/src/keepersdk/proto/BI_pb2.py b/keepersdk-package/src/keepersdk/proto/BI_pb2.py index 15797a52..09b2ff51 100644 --- a/keepersdk-package/src/keepersdk/proto/BI_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/BI_pb2.py @@ -2,29 +2,22 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: BI.proto -# Protobuf Python Version: 5.28.3 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 3, - '', - 'BI.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x08\x42I.proto\x12\x02\x42I\"f\n\x1bValidateSessionTokenRequest\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x1c\n\x14returnMcEnterpiseIds\x18\x02 \x01(\x08\x12\n\n\x02ip\x18\x03 \x01(\t\"\xda\x02\n\x1cValidateSessionTokenResponse\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x0e\n\x06userId\x18\x02 \x01(\x05\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x37\n\x06status\x18\x04 \x01(\x0e\x32\'.BI.ValidateSessionTokenResponse.Status\x12\x15\n\rstatusMessage\x18\x05 \x01(\t\x12\x17\n\x0fmcEnterpriseIds\x18\x06 \x03(\x05\x12\x18\n\x10hasMSPPermission\x18\x07 \x01(\x08\x12\x1e\n\x16\x64\x65letedMcEnterpriseIds\x18\x08 \x03(\x05\"[\n\x06Status\x12\t\n\x05VALID\x10\x00\x12\r\n\tNOT_VALID\x10\x01\x12\x0b\n\x07\x45XPIRED\x10\x02\x12\x0e\n\nIP_BLOCKED\x10\x03\x12\x1a\n\x16INVALID_CLIENT_VERSION\x10\x04\"\x1b\n\x19SubscriptionStatusRequest\"\xa6\x03\n\x1aSubscriptionStatusResponse\x12$\n\x0b\x61utoRenewal\x18\x01 \x01(\x0b\x32\x0f.BI.AutoRenewal\x12/\n\x14\x63urrentPaymentMethod\x18\x02 \x01(\x0b\x32\x11.BI.PaymentMethod\x12\x14\n\x0c\x63heckoutLink\x18\x03 \x01(\t\x12\x19\n\x11licenseCreateDate\x18\x04 \x01(\x03\x12\x15\n\risDistributor\x18\x05 \x01(\x08\x12\x13\n\x0bisLegacyMsp\x18\x06 \x01(\x08\x12&\n\x0clicenseStats\x18\x08 \x03(\x0b\x32\x10.BI.LicenseStats\x12\x35\n\x0egradientStatus\x18\t \x01(\x0e\x32\x1d.BI.GradientIntegrationStatus\x12\x17\n\x0fhideTrialBanner\x18\n \x01(\x08\x12\x1c\n\x14gradientLastSyncDate\x18\x0b \x01(\t\x12\x1c\n\x14gradientNextSyncDate\x18\x0c \x01(\t\x12 \n\x18isGradientMappingPending\x18\r \x01(\x08\"\xd7\x01\n\x0cLicenseStats\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.BI.LicenseStats.Type\x12\x11\n\tavailable\x18\x02 \x01(\x05\x12\x0c\n\x04used\x18\x03 \x01(\x05\"\x80\x01\n\x04Type\x12\x18\n\x14LICENSE_STAT_UNKNOWN\x10\x00\x12\x0c\n\x08MSP_BASE\x10\x01\x12\x0f\n\x0bMC_BUSINESS\x10\x02\x12\x14\n\x10MC_BUSINESS_PLUS\x10\x03\x12\x11\n\rMC_ENTERPRISE\x10\x04\x12\x16\n\x12MC_ENTERPRISE_PLUS\x10\x05\"@\n\x0b\x41utoRenewal\x12\x0e\n\x06nextOn\x18\x01 \x01(\x03\x12\x10\n\x08\x64\x61ysLeft\x18\x02 \x01(\x05\x12\x0f\n\x07isTrial\x18\x03 \x01(\x08\"\x84\x04\n\rPaymentMethod\x12$\n\x04type\x18\x01 \x01(\x0e\x32\x16.BI.PaymentMethod.Type\x12$\n\x04\x63\x61rd\x18\x02 \x01(\x0b\x32\x16.BI.PaymentMethod.Card\x12$\n\x04sepa\x18\x03 \x01(\x0b\x32\x16.BI.PaymentMethod.Sepa\x12(\n\x06paypal\x18\x04 \x01(\x0b\x32\x18.BI.PaymentMethod.Paypal\x12\x15\n\rfailedBilling\x18\x05 \x01(\x08\x12(\n\x06vendor\x18\x06 \x01(\x0b\x32\x18.BI.PaymentMethod.Vendor\x12\x36\n\rpurchaseOrder\x18\x07 \x01(\x0b\x32\x1f.BI.PaymentMethod.PurchaseOrder\x1a$\n\x04\x43\x61rd\x12\r\n\x05last4\x18\x01 \x01(\t\x12\r\n\x05\x62rand\x18\x02 \x01(\t\x1a&\n\x04Sepa\x12\r\n\x05last4\x18\x01 \x01(\t\x12\x0f\n\x07\x63ountry\x18\x02 \x01(\t\x1a\x08\n\x06Paypal\x1a\x16\n\x06Vendor\x12\x0c\n\x04name\x18\x01 \x01(\t\x1a\x1d\n\rPurchaseOrder\x12\x0c\n\x04name\x18\x01 \x01(\t\"O\n\x04Type\x12\x08\n\x04\x43\x41RD\x10\x00\x12\x08\n\x04SEPA\x10\x01\x12\n\n\x06PAYPAL\x10\x02\x12\x08\n\x04NONE\x10\x03\x12\n\n\x06VENDOR\x10\x04\x12\x11\n\rPURCHASEORDER\x10\x05\"\x1f\n\x1dSubscriptionMspPricingRequest\"\\\n\x1eSubscriptionMspPricingResponse\x12\x19\n\x06\x61\x64\x64ons\x18\x02 \x03(\x0b\x32\t.BI.Addon\x12\x1f\n\tfilePlans\x18\x03 \x03(\x0b\x32\x0c.BI.FilePlan\"\x1e\n\x1cSubscriptionMcPricingRequest\"|\n\x1dSubscriptionMcPricingResponse\x12\x1f\n\tbasePlans\x18\x01 \x03(\x0b\x32\x0c.BI.BasePlan\x12\x19\n\x06\x61\x64\x64ons\x18\x02 \x03(\x0b\x32\t.BI.Addon\x12\x1f\n\tfilePlans\x18\x03 \x03(\x0b\x32\x0c.BI.FilePlan\".\n\x08\x42\x61sePlan\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\"C\n\x05\x41\x64\x64on\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\x12\x16\n\x0e\x61mountConsumed\x18\x03 \x01(\x03\".\n\x08\x46ilePlan\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\"\xab\x01\n\x04\x43ost\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x01\x12%\n\tamountPer\x18\x04 \x01(\x0e\x32\x12.BI.Cost.AmountPer\x12\x1e\n\x08\x63urrency\x18\x05 \x01(\x0e\x32\x0c.BI.Currency\"L\n\tAmountPer\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05MONTH\x10\x01\x12\x0e\n\nUSER_MONTH\x10\x02\x12\x17\n\x13USER_CONSUMED_MONTH\x10\x03\"\\\n\x14InvoiceSearchRequest\x12\x0c\n\x04size\x18\x01 \x01(\x05\x12\x17\n\x0fstartingAfterId\x18\x02 \x01(\x05\x12\x1d\n\x15\x61llInvoicesUnfiltered\x18\x03 \x01(\x08\"6\n\x15InvoiceSearchResponse\x12\x1d\n\x08invoices\x18\x01 \x03(\x0b\x32\x0b.BI.Invoice\"\xbe\x02\n\x07Invoice\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rinvoiceNumber\x18\x02 \x01(\t\x12\x13\n\x0binvoiceDate\x18\x03 \x01(\x03\x12\x14\n\x0clicenseCount\x18\x04 \x01(\x05\x12#\n\ttotalCost\x18\x05 \x01(\x0b\x32\x10.BI.Invoice.Cost\x12%\n\x0binvoiceType\x18\x06 \x01(\x0e\x32\x10.BI.Invoice.Type\x1a\x36\n\x04\x43ost\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x01\x12\x1e\n\x08\x63urrency\x18\x02 \x01(\x0e\x32\x0c.BI.Currency\"a\n\x04Type\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07UPGRADE\x10\x03\x12\x0b\n\x07RESTORE\x10\x04\x12\x0f\n\x0b\x41SSOCIATION\x10\x05\x12\x0b\n\x07OVERAGE\x10\x06\"/\n\x16InvoiceDownloadRequest\x12\x15\n\rinvoiceNumber\x18\x01 \x01(\t\"9\n\x17InvoiceDownloadResponse\x12\x0c\n\x04link\x18\x01 \x01(\t\x12\x10\n\x08\x66ileName\x18\x02 \x01(\t\"<\n\x1dReportingDailySnapshotRequest\x12\r\n\x05month\x18\x01 \x01(\x05\x12\x0c\n\x04year\x18\x02 \x01(\x05\"v\n\x1eReportingDailySnapshotResponse\x12#\n\x07records\x18\x01 \x03(\x0b\x32\x12.BI.SnapshotRecord\x12/\n\rmcEnterprises\x18\x02 \x03(\x0b\x32\x18.BI.SnapshotMcEnterprise\"\xd7\x01\n\x0eSnapshotRecord\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\x03\x12\x16\n\x0emcEnterpriseId\x18\x02 \x01(\x05\x12\x17\n\x0fmaxLicenseCount\x18\x04 \x01(\x05\x12\x19\n\x11maxFilePlanTypeId\x18\x05 \x01(\x05\x12\x15\n\rmaxBasePlanId\x18\x06 \x01(\x05\x12(\n\x06\x61\x64\x64ons\x18\x07 \x03(\x0b\x32\x18.BI.SnapshotRecord.Addon\x1a*\n\x05\x41\x64\x64on\x12\x12\n\nmaxAddonId\x18\x01 \x01(\x05\x12\r\n\x05units\x18\x02 \x01(\x03\"0\n\x14SnapshotMcEnterprise\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"\x16\n\x14MappingAddonsRequest\"\\\n\x15MappingAddonsResponse\x12\x1f\n\x06\x61\x64\x64ons\x18\x01 \x03(\x0b\x32\x0f.BI.MappingItem\x12\"\n\tfilePlans\x18\x02 \x03(\x0b\x32\x0f.BI.MappingItem\"\'\n\x0bMappingItem\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"1\n\x1aGradientValidateKeyRequest\x12\x13\n\x0bgradientKey\x18\x01 \x01(\t\"?\n\x1bGradientValidateKeyResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"D\n\x13GradientSaveRequest\x12\x13\n\x0bgradientKey\x18\x01 \x01(\t\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"g\n\x14GradientSaveResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12-\n\x06status\x18\x02 \x01(\x0e\x32\x1d.BI.GradientIntegrationStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\"1\n\x15GradientRemoveRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\":\n\x16GradientRemoveResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"/\n\x13GradientSyncRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\"g\n\x14GradientSyncResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12-\n\x06status\x18\x02 \x01(\x0e\x32\x1d.BI.GradientIntegrationStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\"N\n\'NetPromoterScoreSurveySubmissionRequest\x12\x14\n\x0csurvey_score\x18\x01 \x01(\x05\x12\r\n\x05notes\x18\x02 \x01(\t\"*\n(NetPromoterScoreSurveySubmissionResponse\"&\n$NetPromoterScorePopupScheduleRequest\";\n%NetPromoterScorePopupScheduleResponse\x12\x12\n\nshow_popup\x18\x01 \x01(\x08\"\'\n%NetPromoterScorePopupDismissalRequest\"(\n&NetPromoterScorePopupDismissalResponse\"-\n\x11KCMLicenseRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\"%\n\x12KCMLicenseResponse\x12\x0f\n\x07message\x18\x01 \x01(\t*D\n\x08\x43urrency\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03USD\x10\x01\x12\x07\n\x03GBP\x10\x02\x12\x07\n\x03JPY\x10\x03\x12\x07\n\x03\x45UR\x10\x04\x12\x07\n\x03\x41UD\x10\x05*S\n\x19GradientIntegrationStatus\x12\x10\n\x0cNOTCONNECTED\x10\x00\x12\x0b\n\x07PENDING\x10\x01\x12\r\n\tCONNECTED\x10\x02\x12\x08\n\x04NONE\x10\x03\x42\x1e\n\x18\x63om.keepersecurity.protoB\x02\x42Ib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x08\x42I.proto\x12\x02\x42I\x1a\x1cgoogle/protobuf/struct.proto\"f\n\x1bValidateSessionTokenRequest\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x1c\n\x14returnMcEnterpiseIds\x18\x02 \x01(\x08\x12\n\n\x02ip\x18\x03 \x01(\t\"\xda\x02\n\x1cValidateSessionTokenResponse\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x0e\n\x06userId\x18\x02 \x01(\x05\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x37\n\x06status\x18\x04 \x01(\x0e\x32\'.BI.ValidateSessionTokenResponse.Status\x12\x15\n\rstatusMessage\x18\x05 \x01(\t\x12\x17\n\x0fmcEnterpriseIds\x18\x06 \x03(\x05\x12\x18\n\x10hasMSPPermission\x18\x07 \x01(\x08\x12\x1e\n\x16\x64\x65letedMcEnterpriseIds\x18\x08 \x03(\x05\"[\n\x06Status\x12\t\n\x05VALID\x10\x00\x12\r\n\tNOT_VALID\x10\x01\x12\x0b\n\x07\x45XPIRED\x10\x02\x12\x0e\n\nIP_BLOCKED\x10\x03\x12\x1a\n\x16INVALID_CLIENT_VERSION\x10\x04\"\x1b\n\x19SubscriptionStatusRequest\"\xa6\x03\n\x1aSubscriptionStatusResponse\x12$\n\x0b\x61utoRenewal\x18\x01 \x01(\x0b\x32\x0f.BI.AutoRenewal\x12/\n\x14\x63urrentPaymentMethod\x18\x02 \x01(\x0b\x32\x11.BI.PaymentMethod\x12\x14\n\x0c\x63heckoutLink\x18\x03 \x01(\t\x12\x19\n\x11licenseCreateDate\x18\x04 \x01(\x03\x12\x15\n\risDistributor\x18\x05 \x01(\x08\x12\x13\n\x0bisLegacyMsp\x18\x06 \x01(\x08\x12&\n\x0clicenseStats\x18\x08 \x03(\x0b\x32\x10.BI.LicenseStats\x12\x35\n\x0egradientStatus\x18\t \x01(\x0e\x32\x1d.BI.GradientIntegrationStatus\x12\x17\n\x0fhideTrialBanner\x18\n \x01(\x08\x12\x1c\n\x14gradientLastSyncDate\x18\x0b \x01(\t\x12\x1c\n\x14gradientNextSyncDate\x18\x0c \x01(\t\x12 \n\x18isGradientMappingPending\x18\r \x01(\x08\"\x97\x02\n\x0cLicenseStats\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.BI.LicenseStats.Type\x12\x11\n\tavailable\x18\x02 \x01(\x05\x12\x0c\n\x04used\x18\x03 \x01(\x05\"\xc0\x01\n\x04Type\x12\x18\n\x14LICENSE_STAT_UNKNOWN\x10\x00\x12\x0c\n\x08MSP_BASE\x10\x01\x12\x0f\n\x0bMC_BUSINESS\x10\x02\x12\x14\n\x10MC_BUSINESS_PLUS\x10\x03\x12\x11\n\rMC_ENTERPRISE\x10\x04\x12\x16\n\x12MC_ENTERPRISE_PLUS\x10\x05\x12\x18\n\x14\x42\x32\x42_BUSINESS_STARTER\x10\x06\x12\x10\n\x0c\x42\x32\x42_BUSINESS\x10\x07\x12\x12\n\x0e\x42\x32\x42_ENTERPRISE\x10\x08\"@\n\x0b\x41utoRenewal\x12\x0e\n\x06nextOn\x18\x01 \x01(\x03\x12\x10\n\x08\x64\x61ysLeft\x18\x02 \x01(\x05\x12\x0f\n\x07isTrial\x18\x03 \x01(\x08\"\x84\x04\n\rPaymentMethod\x12$\n\x04type\x18\x01 \x01(\x0e\x32\x16.BI.PaymentMethod.Type\x12$\n\x04\x63\x61rd\x18\x02 \x01(\x0b\x32\x16.BI.PaymentMethod.Card\x12$\n\x04sepa\x18\x03 \x01(\x0b\x32\x16.BI.PaymentMethod.Sepa\x12(\n\x06paypal\x18\x04 \x01(\x0b\x32\x18.BI.PaymentMethod.Paypal\x12\x15\n\rfailedBilling\x18\x05 \x01(\x08\x12(\n\x06vendor\x18\x06 \x01(\x0b\x32\x18.BI.PaymentMethod.Vendor\x12\x36\n\rpurchaseOrder\x18\x07 \x01(\x0b\x32\x1f.BI.PaymentMethod.PurchaseOrder\x1a$\n\x04\x43\x61rd\x12\r\n\x05last4\x18\x01 \x01(\t\x12\r\n\x05\x62rand\x18\x02 \x01(\t\x1a&\n\x04Sepa\x12\r\n\x05last4\x18\x01 \x01(\t\x12\x0f\n\x07\x63ountry\x18\x02 \x01(\t\x1a\x08\n\x06Paypal\x1a\x16\n\x06Vendor\x12\x0c\n\x04name\x18\x01 \x01(\t\x1a\x1d\n\rPurchaseOrder\x12\x0c\n\x04name\x18\x01 \x01(\t\"O\n\x04Type\x12\x08\n\x04\x43\x41RD\x10\x00\x12\x08\n\x04SEPA\x10\x01\x12\n\n\x06PAYPAL\x10\x02\x12\x08\n\x04NONE\x10\x03\x12\n\n\x06VENDOR\x10\x04\x12\x11\n\rPURCHASEORDER\x10\x05\"\x1f\n\x1dSubscriptionMspPricingRequest\"\\\n\x1eSubscriptionMspPricingResponse\x12\x19\n\x06\x61\x64\x64ons\x18\x02 \x03(\x0b\x32\t.BI.Addon\x12\x1f\n\tfilePlans\x18\x03 \x03(\x0b\x32\x0c.BI.FilePlan\"\x1e\n\x1cSubscriptionMcPricingRequest\"|\n\x1dSubscriptionMcPricingResponse\x12\x1f\n\tbasePlans\x18\x01 \x03(\x0b\x32\x0c.BI.BasePlan\x12\x19\n\x06\x61\x64\x64ons\x18\x02 \x03(\x0b\x32\t.BI.Addon\x12\x1f\n\tfilePlans\x18\x03 \x03(\x0b\x32\x0c.BI.FilePlan\".\n\x08\x42\x61sePlan\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\"C\n\x05\x41\x64\x64on\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\x12\x16\n\x0e\x61mountConsumed\x18\x03 \x01(\x03\".\n\x08\x46ilePlan\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\"\x84\x02\n\x04\x43ost\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x01\x12%\n\tamountPer\x18\x04 \x01(\x0e\x32\x12.BI.Cost.AmountPer\x12\x1e\n\x08\x63urrency\x18\x05 \x01(\x0e\x32\x0c.BI.Currency\"\xa4\x01\n\tAmountPer\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05MONTH\x10\x01\x12\x0e\n\nUSER_MONTH\x10\x02\x12\x17\n\x13USER_CONSUMED_MONTH\x10\x03\x12\x12\n\x0e\x45NDPOINT_MONTH\x10\x04\x12\r\n\tUSER_YEAR\x10\x05\x12\x16\n\x12USER_CONSUMED_YEAR\x10\x06\x12\x08\n\x04YEAR\x10\x07\x12\x11\n\rENDPOINT_YEAR\x10\x08\"\\\n\x14InvoiceSearchRequest\x12\x0c\n\x04size\x18\x01 \x01(\x05\x12\x17\n\x0fstartingAfterId\x18\x02 \x01(\x05\x12\x1d\n\x15\x61llInvoicesUnfiltered\x18\x03 \x01(\x08\"6\n\x15InvoiceSearchResponse\x12\x1d\n\x08invoices\x18\x01 \x03(\x0b\x32\x0b.BI.Invoice\"\xbe\x02\n\x07Invoice\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rinvoiceNumber\x18\x02 \x01(\t\x12\x13\n\x0binvoiceDate\x18\x03 \x01(\x03\x12\x14\n\x0clicenseCount\x18\x04 \x01(\x05\x12#\n\ttotalCost\x18\x05 \x01(\x0b\x32\x10.BI.Invoice.Cost\x12%\n\x0binvoiceType\x18\x06 \x01(\x0e\x32\x10.BI.Invoice.Type\x1a\x36\n\x04\x43ost\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x01\x12\x1e\n\x08\x63urrency\x18\x02 \x01(\x0e\x32\x0c.BI.Currency\"a\n\x04Type\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07UPGRADE\x10\x03\x12\x0b\n\x07RESTORE\x10\x04\x12\x0f\n\x0b\x41SSOCIATION\x10\x05\x12\x0b\n\x07OVERAGE\x10\x06\"\x1a\n\x18VaultInvoicesListRequest\"?\n\x19VaultInvoicesListResponse\x12\"\n\x08invoices\x18\x01 \x03(\x0b\x32\x10.BI.VaultInvoice\"\x8f\x01\n\x0cVaultInvoice\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rinvoiceNumber\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x61teCreated\x18\x03 \x01(\x03\x12\x1f\n\x05total\x18\x04 \x01(\x0b\x32\x10.BI.Invoice.Cost\x12&\n\x0cpurchaseType\x18\x05 \x01(\x0e\x32\x10.BI.Invoice.Type\"/\n\x16InvoiceDownloadRequest\x12\x15\n\rinvoiceNumber\x18\x01 \x01(\t\"9\n\x17InvoiceDownloadResponse\x12\x0c\n\x04link\x18\x01 \x01(\t\x12\x10\n\x08\x66ileName\x18\x02 \x01(\t\"8\n\x1fVaultInvoiceDownloadLinkRequest\x12\x15\n\rinvoiceNumber\x18\x01 \x01(\t\"B\n VaultInvoiceDownloadLinkResponse\x12\x0c\n\x04link\x18\x01 \x01(\t\x12\x10\n\x08\x66ileName\x18\x02 \x01(\t\"<\n\x1dReportingDailySnapshotRequest\x12\r\n\x05month\x18\x01 \x01(\x05\x12\x0c\n\x04year\x18\x02 \x01(\x05\"v\n\x1eReportingDailySnapshotResponse\x12#\n\x07records\x18\x01 \x03(\x0b\x32\x12.BI.SnapshotRecord\x12/\n\rmcEnterprises\x18\x02 \x03(\x0b\x32\x18.BI.SnapshotMcEnterprise\"\xd7\x01\n\x0eSnapshotRecord\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\x03\x12\x16\n\x0emcEnterpriseId\x18\x02 \x01(\x05\x12\x17\n\x0fmaxLicenseCount\x18\x04 \x01(\x05\x12\x19\n\x11maxFilePlanTypeId\x18\x05 \x01(\x05\x12\x15\n\rmaxBasePlanId\x18\x06 \x01(\x05\x12(\n\x06\x61\x64\x64ons\x18\x07 \x03(\x0b\x32\x18.BI.SnapshotRecord.Addon\x1a*\n\x05\x41\x64\x64on\x12\x12\n\nmaxAddonId\x18\x01 \x01(\x05\x12\r\n\x05units\x18\x02 \x01(\x03\"0\n\x14SnapshotMcEnterprise\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"\x16\n\x14MappingAddonsRequest\"\\\n\x15MappingAddonsResponse\x12\x1f\n\x06\x61\x64\x64ons\x18\x01 \x03(\x0b\x32\x0f.BI.MappingItem\x12\"\n\tfilePlans\x18\x02 \x03(\x0b\x32\x0f.BI.MappingItem\"\'\n\x0bMappingItem\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"1\n\x1aGradientValidateKeyRequest\x12\x13\n\x0bgradientKey\x18\x01 \x01(\t\"?\n\x1bGradientValidateKeyResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"D\n\x13GradientSaveRequest\x12\x13\n\x0bgradientKey\x18\x01 \x01(\t\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"g\n\x14GradientSaveResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12-\n\x06status\x18\x02 \x01(\x0e\x32\x1d.BI.GradientIntegrationStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\"1\n\x15GradientRemoveRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\":\n\x16GradientRemoveResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"/\n\x13GradientSyncRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\"g\n\x14GradientSyncResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12-\n\x06status\x18\x02 \x01(\x0e\x32\x1d.BI.GradientIntegrationStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\"N\n\'NetPromoterScoreSurveySubmissionRequest\x12\x14\n\x0csurvey_score\x18\x01 \x01(\x05\x12\r\n\x05notes\x18\x02 \x01(\t\"*\n(NetPromoterScoreSurveySubmissionResponse\"&\n$NetPromoterScorePopupScheduleRequest\";\n%NetPromoterScorePopupScheduleResponse\x12\x12\n\nshow_popup\x18\x01 \x01(\x08\"\'\n%NetPromoterScorePopupDismissalRequest\"(\n&NetPromoterScorePopupDismissalResponse\"-\n\x11KCMLicenseRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\"%\n\x12KCMLicenseResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x84\x01\n\x0c\x45ventRequest\x12 \n\teventType\x18\x01 \x01(\x0e\x32\r.BI.EventType\x12\x12\n\neventValue\x18\x02 \x01(\t\x12\x11\n\teventTime\x18\x03 \x01(\x03\x12+\n\nattributes\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"0\n\rEventsRequest\x12\x1f\n\x05\x65vent\x18\x01 \x03(\x0b\x32\x10.BI.EventRequest\"\xa9\x01\n\x16\x43ustomerCaptureRequest\x12\x0f\n\x07pageUrl\x18\x01 \x01(\t\x12\x0c\n\x04tree\x18\x02 \x01(\t\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\r\n\x05image\x18\x04 \x01(\t\x12\x14\n\x0cpageLoadTime\x18\x05 \x01(\t\x12\r\n\x05keyId\x18\x06 \x01(\t\x12\x0c\n\x04test\x18\x07 \x01(\x08\x12\x11\n\tissueType\x18\x08 \x01(\t\x12\r\n\x05notes\x18\t \x01(\t\"\x19\n\x17\x43ustomerCaptureResponse\"|\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12%\n\x06\x65xtras\x18\x03 \x03(\x0b\x32\x15.BI.Error.ExtrasEntry\x1a-\n\x0b\x45xtrasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"z\n\rQuotePurchase\x12\x12\n\nquoteTotal\x18\x01 \x01(\x01\x12\x13\n\x0bincludedTax\x18\x02 \x01(\x08\x12\x1b\n\x13includedOtherAddons\x18\x03 \x01(\x08\x12\x11\n\ttaxAmount\x18\x04 \x01(\x01\x12\x10\n\x08taxLabel\x18\x05 \x01(\t\"\x1d\n\x1bUpgradeLicenseStatusRequest\"p\n\x1cUpgradeLicenseStatusResponse\x12 \n\x18\x61llowPurchaseFromConsole\x18\x01 \x01(\x08\x12\x14\n\x0c\x63heckoutLink\x18\x02 \x01(\t\x12\x18\n\x05\x65rror\x18\x03 \x01(\x0b\x32\t.BI.Error\"d\n\"UpgradeLicenseQuotePurchaseRequest\x12,\n\x0bproductType\x18\x01 \x01(\x0e\x32\x17.BI.PurchaseProductType\x12\x10\n\x08quantity\x18\x02 \x01(\x05\"\x93\x01\n#UpgradeLicenseQuotePurchaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12(\n\rquotePurchase\x18\x02 \x01(\x0b\x32\x11.BI.QuotePurchase\x12\x17\n\x0fviewSummaryLink\x18\x03 \x01(\t\x12\x18\n\x05\x65rror\x18\x04 \x01(\x0b\x32\t.BI.Error\"\x91\x01\n%UpgradeLicenseCompletePurchaseRequest\x12,\n\x0bproductType\x18\x01 \x01(\x0e\x32\x17.BI.PurchaseProductType\x12\x10\n\x08quantity\x18\x02 \x01(\x05\x12(\n\rquotePurchase\x18\x03 \x01(\x0b\x32\x11.BI.QuotePurchase\"\x94\x01\n&UpgradeLicenseCompletePurchaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rinvoiceNumber\x18\x02 \x01(\t\x12\x18\n\x05\x65rror\x18\x03 \x01(\x0b\x32\t.BI.Error\x12(\n\rquotePurchase\x18\x04 \x01(\x0b\x32\x11.BI.QuotePurchase\"\xd5\x01\n\x12\x45nterpriseBasePlan\x12I\n\x0f\x62\x61seplanVersion\x18\x01 \x01(\x0e\x32\x30.BI.EnterpriseBasePlan.EnterpriseBasePlanVersion\x12\x16\n\x04\x63ost\x18\x02 \x01(\x0b\x32\x08.BI.Cost\"\\\n\x19\x45nterpriseBasePlanVersion\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x14\n\x10\x42USINESS_STARTER\x10\x01\x12\x0c\n\x08\x42USINESS\x10\x02\x12\x0e\n\nENTERPRISE\x10\x03\"&\n$SubscriptionEnterprisePricingRequest\"\x8e\x01\n%SubscriptionEnterprisePricingResponse\x12)\n\tbasePlans\x18\x01 \x03(\x0b\x32\x16.BI.EnterpriseBasePlan\x12\x19\n\x06\x61\x64\x64ons\x18\x02 \x03(\x0b\x32\t.BI.Addon\x12\x1f\n\tfilePlans\x18\x03 \x03(\x0b\x32\x0c.BI.FilePlan*M\n\x08\x43urrency\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03USD\x10\x01\x12\x07\n\x03GBP\x10\x02\x12\x07\n\x03JPY\x10\x03\x12\x07\n\x03\x45UR\x10\x04\x12\x07\n\x03\x41UD\x10\x05\x12\x07\n\x03\x43\x41\x44\x10\x06*S\n\x19GradientIntegrationStatus\x12\x10\n\x0cNOTCONNECTED\x10\x00\x12\x0b\n\x07PENDING\x10\x01\x12\r\n\tCONNECTED\x10\x02\x12\x08\n\x04NONE\x10\x03*\xdf\x01\n\tEventType\x12\x1f\n\x1bUNKNOWN_TRACKING_EVENT_TYPE\x10\x00\x12\x1c\n\x18TRACKING_POPUP_DISPLAYED\x10\x01\x12\x1b\n\x17TRACKING_POPUP_ACCEPTED\x10\x02\x12\x1c\n\x18TRACKING_POPUP_DISMISSED\x10\x03\x12\x17\n\x13TRACKING_POPUP_PAID\x10\x04\x12\x19\n\x15TRACKING_PUSH_CLICKED\x10\x05\x12\x12\n\x0e\x43ONSOLE_ACTION\x10\x06\x12\x10\n\x0cVAULT_ACTION\x10\x07*\xc8\x01\n\x13PurchaseProductType\x12\x17\n\x13upgradeToEnterprise\x10\x00\x12\x0c\n\x08\x61\x64\x64Users\x10\x01\x12\x0e\n\naddStorage\x10\x02\x12\x0c\n\x08\x61\x64\x64\x41udit\x10\x03\x12\x12\n\x0e\x61\x64\x64\x42reachWatch\x10\x04\x12\x11\n\raddCompliance\x10\x05\x12\x0b\n\x07\x61\x64\x64\x43hat\x10\x06\x12\n\n\x06\x61\x64\x64PAM\x10\x07\x12\x14\n\x10\x61\x64\x64SilverSupport\x10\x08\x12\x16\n\x12\x61\x64\x64PlatinumSupport\x10\tB\x1e\n\x18\x63om.keepersecurity.protoB\x02\x42Ib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -32,118 +25,168 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\002BI' - _globals['_CURRENCY']._serialized_start=4488 - _globals['_CURRENCY']._serialized_end=4556 - _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_start=4558 - _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_end=4641 - _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_start=16 - _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_end=118 - _globals['_VALIDATESESSIONTOKENRESPONSE']._serialized_start=121 - _globals['_VALIDATESESSIONTOKENRESPONSE']._serialized_end=467 - _globals['_VALIDATESESSIONTOKENRESPONSE_STATUS']._serialized_start=376 - _globals['_VALIDATESESSIONTOKENRESPONSE_STATUS']._serialized_end=467 - _globals['_SUBSCRIPTIONSTATUSREQUEST']._serialized_start=469 - _globals['_SUBSCRIPTIONSTATUSREQUEST']._serialized_end=496 - _globals['_SUBSCRIPTIONSTATUSRESPONSE']._serialized_start=499 - _globals['_SUBSCRIPTIONSTATUSRESPONSE']._serialized_end=921 - _globals['_LICENSESTATS']._serialized_start=924 - _globals['_LICENSESTATS']._serialized_end=1139 - _globals['_LICENSESTATS_TYPE']._serialized_start=1011 - _globals['_LICENSESTATS_TYPE']._serialized_end=1139 - _globals['_AUTORENEWAL']._serialized_start=1141 - _globals['_AUTORENEWAL']._serialized_end=1205 - _globals['_PAYMENTMETHOD']._serialized_start=1208 - _globals['_PAYMENTMETHOD']._serialized_end=1724 - _globals['_PAYMENTMETHOD_CARD']._serialized_start=1502 - _globals['_PAYMENTMETHOD_CARD']._serialized_end=1538 - _globals['_PAYMENTMETHOD_SEPA']._serialized_start=1540 - _globals['_PAYMENTMETHOD_SEPA']._serialized_end=1578 - _globals['_PAYMENTMETHOD_PAYPAL']._serialized_start=1580 - _globals['_PAYMENTMETHOD_PAYPAL']._serialized_end=1588 - _globals['_PAYMENTMETHOD_VENDOR']._serialized_start=1590 - _globals['_PAYMENTMETHOD_VENDOR']._serialized_end=1612 - _globals['_PAYMENTMETHOD_PURCHASEORDER']._serialized_start=1614 - _globals['_PAYMENTMETHOD_PURCHASEORDER']._serialized_end=1643 - _globals['_PAYMENTMETHOD_TYPE']._serialized_start=1645 - _globals['_PAYMENTMETHOD_TYPE']._serialized_end=1724 - _globals['_SUBSCRIPTIONMSPPRICINGREQUEST']._serialized_start=1726 - _globals['_SUBSCRIPTIONMSPPRICINGREQUEST']._serialized_end=1757 - _globals['_SUBSCRIPTIONMSPPRICINGRESPONSE']._serialized_start=1759 - _globals['_SUBSCRIPTIONMSPPRICINGRESPONSE']._serialized_end=1851 - _globals['_SUBSCRIPTIONMCPRICINGREQUEST']._serialized_start=1853 - _globals['_SUBSCRIPTIONMCPRICINGREQUEST']._serialized_end=1883 - _globals['_SUBSCRIPTIONMCPRICINGRESPONSE']._serialized_start=1885 - _globals['_SUBSCRIPTIONMCPRICINGRESPONSE']._serialized_end=2009 - _globals['_BASEPLAN']._serialized_start=2011 - _globals['_BASEPLAN']._serialized_end=2057 - _globals['_ADDON']._serialized_start=2059 - _globals['_ADDON']._serialized_end=2126 - _globals['_FILEPLAN']._serialized_start=2128 - _globals['_FILEPLAN']._serialized_end=2174 - _globals['_COST']._serialized_start=2177 - _globals['_COST']._serialized_end=2348 - _globals['_COST_AMOUNTPER']._serialized_start=2272 - _globals['_COST_AMOUNTPER']._serialized_end=2348 - _globals['_INVOICESEARCHREQUEST']._serialized_start=2350 - _globals['_INVOICESEARCHREQUEST']._serialized_end=2442 - _globals['_INVOICESEARCHRESPONSE']._serialized_start=2444 - _globals['_INVOICESEARCHRESPONSE']._serialized_end=2498 - _globals['_INVOICE']._serialized_start=2501 - _globals['_INVOICE']._serialized_end=2819 - _globals['_INVOICE_COST']._serialized_start=2666 - _globals['_INVOICE_COST']._serialized_end=2720 - _globals['_INVOICE_TYPE']._serialized_start=2722 - _globals['_INVOICE_TYPE']._serialized_end=2819 - _globals['_INVOICEDOWNLOADREQUEST']._serialized_start=2821 - _globals['_INVOICEDOWNLOADREQUEST']._serialized_end=2868 - _globals['_INVOICEDOWNLOADRESPONSE']._serialized_start=2870 - _globals['_INVOICEDOWNLOADRESPONSE']._serialized_end=2927 - _globals['_REPORTINGDAILYSNAPSHOTREQUEST']._serialized_start=2929 - _globals['_REPORTINGDAILYSNAPSHOTREQUEST']._serialized_end=2989 - _globals['_REPORTINGDAILYSNAPSHOTRESPONSE']._serialized_start=2991 - _globals['_REPORTINGDAILYSNAPSHOTRESPONSE']._serialized_end=3109 - _globals['_SNAPSHOTRECORD']._serialized_start=3112 - _globals['_SNAPSHOTRECORD']._serialized_end=3327 - _globals['_SNAPSHOTRECORD_ADDON']._serialized_start=3285 - _globals['_SNAPSHOTRECORD_ADDON']._serialized_end=3327 - _globals['_SNAPSHOTMCENTERPRISE']._serialized_start=3329 - _globals['_SNAPSHOTMCENTERPRISE']._serialized_end=3377 - _globals['_MAPPINGADDONSREQUEST']._serialized_start=3379 - _globals['_MAPPINGADDONSREQUEST']._serialized_end=3401 - _globals['_MAPPINGADDONSRESPONSE']._serialized_start=3403 - _globals['_MAPPINGADDONSRESPONSE']._serialized_end=3495 - _globals['_MAPPINGITEM']._serialized_start=3497 - _globals['_MAPPINGITEM']._serialized_end=3536 - _globals['_GRADIENTVALIDATEKEYREQUEST']._serialized_start=3538 - _globals['_GRADIENTVALIDATEKEYREQUEST']._serialized_end=3587 - _globals['_GRADIENTVALIDATEKEYRESPONSE']._serialized_start=3589 - _globals['_GRADIENTVALIDATEKEYRESPONSE']._serialized_end=3652 - _globals['_GRADIENTSAVEREQUEST']._serialized_start=3654 - _globals['_GRADIENTSAVEREQUEST']._serialized_end=3722 - _globals['_GRADIENTSAVERESPONSE']._serialized_start=3724 - _globals['_GRADIENTSAVERESPONSE']._serialized_end=3827 - _globals['_GRADIENTREMOVEREQUEST']._serialized_start=3829 - _globals['_GRADIENTREMOVEREQUEST']._serialized_end=3878 - _globals['_GRADIENTREMOVERESPONSE']._serialized_start=3880 - _globals['_GRADIENTREMOVERESPONSE']._serialized_end=3938 - _globals['_GRADIENTSYNCREQUEST']._serialized_start=3940 - _globals['_GRADIENTSYNCREQUEST']._serialized_end=3987 - _globals['_GRADIENTSYNCRESPONSE']._serialized_start=3989 - _globals['_GRADIENTSYNCRESPONSE']._serialized_end=4092 - _globals['_NETPROMOTERSCORESURVEYSUBMISSIONREQUEST']._serialized_start=4094 - _globals['_NETPROMOTERSCORESURVEYSUBMISSIONREQUEST']._serialized_end=4172 - _globals['_NETPROMOTERSCORESURVEYSUBMISSIONRESPONSE']._serialized_start=4174 - _globals['_NETPROMOTERSCORESURVEYSUBMISSIONRESPONSE']._serialized_end=4216 - _globals['_NETPROMOTERSCOREPOPUPSCHEDULEREQUEST']._serialized_start=4218 - _globals['_NETPROMOTERSCOREPOPUPSCHEDULEREQUEST']._serialized_end=4256 - _globals['_NETPROMOTERSCOREPOPUPSCHEDULERESPONSE']._serialized_start=4258 - _globals['_NETPROMOTERSCOREPOPUPSCHEDULERESPONSE']._serialized_end=4317 - _globals['_NETPROMOTERSCOREPOPUPDISMISSALREQUEST']._serialized_start=4319 - _globals['_NETPROMOTERSCOREPOPUPDISMISSALREQUEST']._serialized_end=4358 - _globals['_NETPROMOTERSCOREPOPUPDISMISSALRESPONSE']._serialized_start=4360 - _globals['_NETPROMOTERSCOREPOPUPDISMISSALRESPONSE']._serialized_end=4400 - _globals['_KCMLICENSEREQUEST']._serialized_start=4402 - _globals['_KCMLICENSEREQUEST']._serialized_end=4447 - _globals['_KCMLICENSERESPONSE']._serialized_start=4449 - _globals['_KCMLICENSERESPONSE']._serialized_end=4486 + _globals['_ERROR_EXTRASENTRY']._loaded_options = None + _globals['_ERROR_EXTRASENTRY']._serialized_options = b'8\001' + _globals['_CURRENCY']._serialized_start=6767 + _globals['_CURRENCY']._serialized_end=6844 + _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_start=6846 + _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_end=6929 + _globals['_EVENTTYPE']._serialized_start=6932 + _globals['_EVENTTYPE']._serialized_end=7155 + _globals['_PURCHASEPRODUCTTYPE']._serialized_start=7158 + _globals['_PURCHASEPRODUCTTYPE']._serialized_end=7358 + _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_start=46 + _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_end=148 + _globals['_VALIDATESESSIONTOKENRESPONSE']._serialized_start=151 + _globals['_VALIDATESESSIONTOKENRESPONSE']._serialized_end=497 + _globals['_VALIDATESESSIONTOKENRESPONSE_STATUS']._serialized_start=406 + _globals['_VALIDATESESSIONTOKENRESPONSE_STATUS']._serialized_end=497 + _globals['_SUBSCRIPTIONSTATUSREQUEST']._serialized_start=499 + _globals['_SUBSCRIPTIONSTATUSREQUEST']._serialized_end=526 + _globals['_SUBSCRIPTIONSTATUSRESPONSE']._serialized_start=529 + _globals['_SUBSCRIPTIONSTATUSRESPONSE']._serialized_end=951 + _globals['_LICENSESTATS']._serialized_start=954 + _globals['_LICENSESTATS']._serialized_end=1233 + _globals['_LICENSESTATS_TYPE']._serialized_start=1041 + _globals['_LICENSESTATS_TYPE']._serialized_end=1233 + _globals['_AUTORENEWAL']._serialized_start=1235 + _globals['_AUTORENEWAL']._serialized_end=1299 + _globals['_PAYMENTMETHOD']._serialized_start=1302 + _globals['_PAYMENTMETHOD']._serialized_end=1818 + _globals['_PAYMENTMETHOD_CARD']._serialized_start=1596 + _globals['_PAYMENTMETHOD_CARD']._serialized_end=1632 + _globals['_PAYMENTMETHOD_SEPA']._serialized_start=1634 + _globals['_PAYMENTMETHOD_SEPA']._serialized_end=1672 + _globals['_PAYMENTMETHOD_PAYPAL']._serialized_start=1674 + _globals['_PAYMENTMETHOD_PAYPAL']._serialized_end=1682 + _globals['_PAYMENTMETHOD_VENDOR']._serialized_start=1684 + _globals['_PAYMENTMETHOD_VENDOR']._serialized_end=1706 + _globals['_PAYMENTMETHOD_PURCHASEORDER']._serialized_start=1708 + _globals['_PAYMENTMETHOD_PURCHASEORDER']._serialized_end=1737 + _globals['_PAYMENTMETHOD_TYPE']._serialized_start=1739 + _globals['_PAYMENTMETHOD_TYPE']._serialized_end=1818 + _globals['_SUBSCRIPTIONMSPPRICINGREQUEST']._serialized_start=1820 + _globals['_SUBSCRIPTIONMSPPRICINGREQUEST']._serialized_end=1851 + _globals['_SUBSCRIPTIONMSPPRICINGRESPONSE']._serialized_start=1853 + _globals['_SUBSCRIPTIONMSPPRICINGRESPONSE']._serialized_end=1945 + _globals['_SUBSCRIPTIONMCPRICINGREQUEST']._serialized_start=1947 + _globals['_SUBSCRIPTIONMCPRICINGREQUEST']._serialized_end=1977 + _globals['_SUBSCRIPTIONMCPRICINGRESPONSE']._serialized_start=1979 + _globals['_SUBSCRIPTIONMCPRICINGRESPONSE']._serialized_end=2103 + _globals['_BASEPLAN']._serialized_start=2105 + _globals['_BASEPLAN']._serialized_end=2151 + _globals['_ADDON']._serialized_start=2153 + _globals['_ADDON']._serialized_end=2220 + _globals['_FILEPLAN']._serialized_start=2222 + _globals['_FILEPLAN']._serialized_end=2268 + _globals['_COST']._serialized_start=2271 + _globals['_COST']._serialized_end=2531 + _globals['_COST_AMOUNTPER']._serialized_start=2367 + _globals['_COST_AMOUNTPER']._serialized_end=2531 + _globals['_INVOICESEARCHREQUEST']._serialized_start=2533 + _globals['_INVOICESEARCHREQUEST']._serialized_end=2625 + _globals['_INVOICESEARCHRESPONSE']._serialized_start=2627 + _globals['_INVOICESEARCHRESPONSE']._serialized_end=2681 + _globals['_INVOICE']._serialized_start=2684 + _globals['_INVOICE']._serialized_end=3002 + _globals['_INVOICE_COST']._serialized_start=2849 + _globals['_INVOICE_COST']._serialized_end=2903 + _globals['_INVOICE_TYPE']._serialized_start=2905 + _globals['_INVOICE_TYPE']._serialized_end=3002 + _globals['_VAULTINVOICESLISTREQUEST']._serialized_start=3004 + _globals['_VAULTINVOICESLISTREQUEST']._serialized_end=3030 + _globals['_VAULTINVOICESLISTRESPONSE']._serialized_start=3032 + _globals['_VAULTINVOICESLISTRESPONSE']._serialized_end=3095 + _globals['_VAULTINVOICE']._serialized_start=3098 + _globals['_VAULTINVOICE']._serialized_end=3241 + _globals['_INVOICEDOWNLOADREQUEST']._serialized_start=3243 + _globals['_INVOICEDOWNLOADREQUEST']._serialized_end=3290 + _globals['_INVOICEDOWNLOADRESPONSE']._serialized_start=3292 + _globals['_INVOICEDOWNLOADRESPONSE']._serialized_end=3349 + _globals['_VAULTINVOICEDOWNLOADLINKREQUEST']._serialized_start=3351 + _globals['_VAULTINVOICEDOWNLOADLINKREQUEST']._serialized_end=3407 + _globals['_VAULTINVOICEDOWNLOADLINKRESPONSE']._serialized_start=3409 + _globals['_VAULTINVOICEDOWNLOADLINKRESPONSE']._serialized_end=3475 + _globals['_REPORTINGDAILYSNAPSHOTREQUEST']._serialized_start=3477 + _globals['_REPORTINGDAILYSNAPSHOTREQUEST']._serialized_end=3537 + _globals['_REPORTINGDAILYSNAPSHOTRESPONSE']._serialized_start=3539 + _globals['_REPORTINGDAILYSNAPSHOTRESPONSE']._serialized_end=3657 + _globals['_SNAPSHOTRECORD']._serialized_start=3660 + _globals['_SNAPSHOTRECORD']._serialized_end=3875 + _globals['_SNAPSHOTRECORD_ADDON']._serialized_start=3833 + _globals['_SNAPSHOTRECORD_ADDON']._serialized_end=3875 + _globals['_SNAPSHOTMCENTERPRISE']._serialized_start=3877 + _globals['_SNAPSHOTMCENTERPRISE']._serialized_end=3925 + _globals['_MAPPINGADDONSREQUEST']._serialized_start=3927 + _globals['_MAPPINGADDONSREQUEST']._serialized_end=3949 + _globals['_MAPPINGADDONSRESPONSE']._serialized_start=3951 + _globals['_MAPPINGADDONSRESPONSE']._serialized_end=4043 + _globals['_MAPPINGITEM']._serialized_start=4045 + _globals['_MAPPINGITEM']._serialized_end=4084 + _globals['_GRADIENTVALIDATEKEYREQUEST']._serialized_start=4086 + _globals['_GRADIENTVALIDATEKEYREQUEST']._serialized_end=4135 + _globals['_GRADIENTVALIDATEKEYRESPONSE']._serialized_start=4137 + _globals['_GRADIENTVALIDATEKEYRESPONSE']._serialized_end=4200 + _globals['_GRADIENTSAVEREQUEST']._serialized_start=4202 + _globals['_GRADIENTSAVEREQUEST']._serialized_end=4270 + _globals['_GRADIENTSAVERESPONSE']._serialized_start=4272 + _globals['_GRADIENTSAVERESPONSE']._serialized_end=4375 + _globals['_GRADIENTREMOVEREQUEST']._serialized_start=4377 + _globals['_GRADIENTREMOVEREQUEST']._serialized_end=4426 + _globals['_GRADIENTREMOVERESPONSE']._serialized_start=4428 + _globals['_GRADIENTREMOVERESPONSE']._serialized_end=4486 + _globals['_GRADIENTSYNCREQUEST']._serialized_start=4488 + _globals['_GRADIENTSYNCREQUEST']._serialized_end=4535 + _globals['_GRADIENTSYNCRESPONSE']._serialized_start=4537 + _globals['_GRADIENTSYNCRESPONSE']._serialized_end=4640 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONREQUEST']._serialized_start=4642 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONREQUEST']._serialized_end=4720 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONRESPONSE']._serialized_start=4722 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONRESPONSE']._serialized_end=4764 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULEREQUEST']._serialized_start=4766 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULEREQUEST']._serialized_end=4804 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULERESPONSE']._serialized_start=4806 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULERESPONSE']._serialized_end=4865 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALREQUEST']._serialized_start=4867 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALREQUEST']._serialized_end=4906 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALRESPONSE']._serialized_start=4908 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALRESPONSE']._serialized_end=4948 + _globals['_KCMLICENSEREQUEST']._serialized_start=4950 + _globals['_KCMLICENSEREQUEST']._serialized_end=4995 + _globals['_KCMLICENSERESPONSE']._serialized_start=4997 + _globals['_KCMLICENSERESPONSE']._serialized_end=5034 + _globals['_EVENTREQUEST']._serialized_start=5037 + _globals['_EVENTREQUEST']._serialized_end=5169 + _globals['_EVENTSREQUEST']._serialized_start=5171 + _globals['_EVENTSREQUEST']._serialized_end=5219 + _globals['_CUSTOMERCAPTUREREQUEST']._serialized_start=5222 + _globals['_CUSTOMERCAPTUREREQUEST']._serialized_end=5391 + _globals['_CUSTOMERCAPTURERESPONSE']._serialized_start=5393 + _globals['_CUSTOMERCAPTURERESPONSE']._serialized_end=5418 + _globals['_ERROR']._serialized_start=5420 + _globals['_ERROR']._serialized_end=5544 + _globals['_ERROR_EXTRASENTRY']._serialized_start=5499 + _globals['_ERROR_EXTRASENTRY']._serialized_end=5544 + _globals['_QUOTEPURCHASE']._serialized_start=5546 + _globals['_QUOTEPURCHASE']._serialized_end=5668 + _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_start=5670 + _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_end=5699 + _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_start=5701 + _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_end=5813 + _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_start=5815 + _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_end=5915 + _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_start=5918 + _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_end=6065 + _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_start=6068 + _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_end=6213 + _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_start=6216 + _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_end=6364 + _globals['_ENTERPRISEBASEPLAN']._serialized_start=6367 + _globals['_ENTERPRISEBASEPLAN']._serialized_end=6580 + _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_start=6488 + _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_end=6580 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_start=6582 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_end=6620 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_start=6623 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_end=6765 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi b/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi index b18fd521..b8e4b319 100644 --- a/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi @@ -1,3 +1,4 @@ +from google.protobuf import struct_pb2 as _struct_pb2 from google.protobuf.internal import containers as _containers from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor @@ -14,6 +15,7 @@ class Currency(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): JPY: _ClassVar[Currency] EUR: _ClassVar[Currency] AUD: _ClassVar[Currency] + CAD: _ClassVar[Currency] class GradientIntegrationStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -21,16 +23,59 @@ class GradientIntegrationStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrappe PENDING: _ClassVar[GradientIntegrationStatus] CONNECTED: _ClassVar[GradientIntegrationStatus] NONE: _ClassVar[GradientIntegrationStatus] + +class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UNKNOWN_TRACKING_EVENT_TYPE: _ClassVar[EventType] + TRACKING_POPUP_DISPLAYED: _ClassVar[EventType] + TRACKING_POPUP_ACCEPTED: _ClassVar[EventType] + TRACKING_POPUP_DISMISSED: _ClassVar[EventType] + TRACKING_POPUP_PAID: _ClassVar[EventType] + TRACKING_PUSH_CLICKED: _ClassVar[EventType] + CONSOLE_ACTION: _ClassVar[EventType] + VAULT_ACTION: _ClassVar[EventType] + +class PurchaseProductType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + upgradeToEnterprise: _ClassVar[PurchaseProductType] + addUsers: _ClassVar[PurchaseProductType] + addStorage: _ClassVar[PurchaseProductType] + addAudit: _ClassVar[PurchaseProductType] + addBreachWatch: _ClassVar[PurchaseProductType] + addCompliance: _ClassVar[PurchaseProductType] + addChat: _ClassVar[PurchaseProductType] + addPAM: _ClassVar[PurchaseProductType] + addSilverSupport: _ClassVar[PurchaseProductType] + addPlatinumSupport: _ClassVar[PurchaseProductType] UNKNOWN: Currency USD: Currency GBP: Currency JPY: Currency EUR: Currency AUD: Currency +CAD: Currency NOTCONNECTED: GradientIntegrationStatus PENDING: GradientIntegrationStatus CONNECTED: GradientIntegrationStatus NONE: GradientIntegrationStatus +UNKNOWN_TRACKING_EVENT_TYPE: EventType +TRACKING_POPUP_DISPLAYED: EventType +TRACKING_POPUP_ACCEPTED: EventType +TRACKING_POPUP_DISMISSED: EventType +TRACKING_POPUP_PAID: EventType +TRACKING_PUSH_CLICKED: EventType +CONSOLE_ACTION: EventType +VAULT_ACTION: EventType +upgradeToEnterprise: PurchaseProductType +addUsers: PurchaseProductType +addStorage: PurchaseProductType +addAudit: PurchaseProductType +addBreachWatch: PurchaseProductType +addCompliance: PurchaseProductType +addChat: PurchaseProductType +addPAM: PurchaseProductType +addSilverSupport: PurchaseProductType +addPlatinumSupport: PurchaseProductType class ValidateSessionTokenRequest(_message.Message): __slots__ = ("encryptedSessionToken", "returnMcEnterpiseIds", "ip") @@ -116,12 +161,18 @@ class LicenseStats(_message.Message): MC_BUSINESS_PLUS: _ClassVar[LicenseStats.Type] MC_ENTERPRISE: _ClassVar[LicenseStats.Type] MC_ENTERPRISE_PLUS: _ClassVar[LicenseStats.Type] + B2B_BUSINESS_STARTER: _ClassVar[LicenseStats.Type] + B2B_BUSINESS: _ClassVar[LicenseStats.Type] + B2B_ENTERPRISE: _ClassVar[LicenseStats.Type] LICENSE_STAT_UNKNOWN: LicenseStats.Type MSP_BASE: LicenseStats.Type MC_BUSINESS: LicenseStats.Type MC_BUSINESS_PLUS: LicenseStats.Type MC_ENTERPRISE: LicenseStats.Type MC_ENTERPRISE_PLUS: LicenseStats.Type + B2B_BUSINESS_STARTER: LicenseStats.Type + B2B_BUSINESS: LicenseStats.Type + B2B_ENTERPRISE: LicenseStats.Type TYPE_FIELD_NUMBER: _ClassVar[int] AVAILABLE_FIELD_NUMBER: _ClassVar[int] USED_FIELD_NUMBER: _ClassVar[int] @@ -259,10 +310,20 @@ class Cost(_message.Message): MONTH: _ClassVar[Cost.AmountPer] USER_MONTH: _ClassVar[Cost.AmountPer] USER_CONSUMED_MONTH: _ClassVar[Cost.AmountPer] + ENDPOINT_MONTH: _ClassVar[Cost.AmountPer] + USER_YEAR: _ClassVar[Cost.AmountPer] + USER_CONSUMED_YEAR: _ClassVar[Cost.AmountPer] + YEAR: _ClassVar[Cost.AmountPer] + ENDPOINT_YEAR: _ClassVar[Cost.AmountPer] UNKNOWN: Cost.AmountPer MONTH: Cost.AmountPer USER_MONTH: Cost.AmountPer USER_CONSUMED_MONTH: Cost.AmountPer + ENDPOINT_MONTH: Cost.AmountPer + USER_YEAR: Cost.AmountPer + USER_CONSUMED_YEAR: Cost.AmountPer + YEAR: Cost.AmountPer + ENDPOINT_YEAR: Cost.AmountPer AMOUNT_FIELD_NUMBER: _ClassVar[int] AMOUNTPER_FIELD_NUMBER: _ClassVar[int] CURRENCY_FIELD_NUMBER: _ClassVar[int] @@ -326,6 +387,30 @@ class Invoice(_message.Message): invoiceType: Invoice.Type def __init__(self, id: _Optional[int] = ..., invoiceNumber: _Optional[str] = ..., invoiceDate: _Optional[int] = ..., licenseCount: _Optional[int] = ..., totalCost: _Optional[_Union[Invoice.Cost, _Mapping]] = ..., invoiceType: _Optional[_Union[Invoice.Type, str]] = ...) -> None: ... +class VaultInvoicesListRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class VaultInvoicesListResponse(_message.Message): + __slots__ = ("invoices",) + INVOICES_FIELD_NUMBER: _ClassVar[int] + invoices: _containers.RepeatedCompositeFieldContainer[VaultInvoice] + def __init__(self, invoices: _Optional[_Iterable[_Union[VaultInvoice, _Mapping]]] = ...) -> None: ... + +class VaultInvoice(_message.Message): + __slots__ = ("id", "invoiceNumber", "dateCreated", "total", "purchaseType") + ID_FIELD_NUMBER: _ClassVar[int] + INVOICENUMBER_FIELD_NUMBER: _ClassVar[int] + DATECREATED_FIELD_NUMBER: _ClassVar[int] + TOTAL_FIELD_NUMBER: _ClassVar[int] + PURCHASETYPE_FIELD_NUMBER: _ClassVar[int] + id: int + invoiceNumber: str + dateCreated: int + total: Invoice.Cost + purchaseType: Invoice.Type + def __init__(self, id: _Optional[int] = ..., invoiceNumber: _Optional[str] = ..., dateCreated: _Optional[int] = ..., total: _Optional[_Union[Invoice.Cost, _Mapping]] = ..., purchaseType: _Optional[_Union[Invoice.Type, str]] = ...) -> None: ... + class InvoiceDownloadRequest(_message.Message): __slots__ = ("invoiceNumber",) INVOICENUMBER_FIELD_NUMBER: _ClassVar[int] @@ -340,6 +425,20 @@ class InvoiceDownloadResponse(_message.Message): fileName: str def __init__(self, link: _Optional[str] = ..., fileName: _Optional[str] = ...) -> None: ... +class VaultInvoiceDownloadLinkRequest(_message.Message): + __slots__ = ("invoiceNumber",) + INVOICENUMBER_FIELD_NUMBER: _ClassVar[int] + invoiceNumber: str + def __init__(self, invoiceNumber: _Optional[str] = ...) -> None: ... + +class VaultInvoiceDownloadLinkResponse(_message.Message): + __slots__ = ("link", "fileName") + LINK_FIELD_NUMBER: _ClassVar[int] + FILENAME_FIELD_NUMBER: _ClassVar[int] + link: str + fileName: str + def __init__(self, link: _Optional[str] = ..., fileName: _Optional[str] = ...) -> None: ... + class ReportingDailySnapshotRequest(_message.Message): __slots__ = ("month", "year") MONTH_FIELD_NUMBER: _ClassVar[int] @@ -510,3 +609,166 @@ class KCMLicenseResponse(_message.Message): MESSAGE_FIELD_NUMBER: _ClassVar[int] message: str def __init__(self, message: _Optional[str] = ...) -> None: ... + +class EventRequest(_message.Message): + __slots__ = ("eventType", "eventValue", "eventTime", "attributes") + EVENTTYPE_FIELD_NUMBER: _ClassVar[int] + EVENTVALUE_FIELD_NUMBER: _ClassVar[int] + EVENTTIME_FIELD_NUMBER: _ClassVar[int] + ATTRIBUTES_FIELD_NUMBER: _ClassVar[int] + eventType: EventType + eventValue: str + eventTime: int + attributes: _struct_pb2.Struct + def __init__(self, eventType: _Optional[_Union[EventType, str]] = ..., eventValue: _Optional[str] = ..., eventTime: _Optional[int] = ..., attributes: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... + +class EventsRequest(_message.Message): + __slots__ = ("event",) + EVENT_FIELD_NUMBER: _ClassVar[int] + event: _containers.RepeatedCompositeFieldContainer[EventRequest] + def __init__(self, event: _Optional[_Iterable[_Union[EventRequest, _Mapping]]] = ...) -> None: ... + +class CustomerCaptureRequest(_message.Message): + __slots__ = ("pageUrl", "tree", "hash", "image", "pageLoadTime", "keyId", "test", "issueType", "notes") + PAGEURL_FIELD_NUMBER: _ClassVar[int] + TREE_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + IMAGE_FIELD_NUMBER: _ClassVar[int] + PAGELOADTIME_FIELD_NUMBER: _ClassVar[int] + KEYID_FIELD_NUMBER: _ClassVar[int] + TEST_FIELD_NUMBER: _ClassVar[int] + ISSUETYPE_FIELD_NUMBER: _ClassVar[int] + NOTES_FIELD_NUMBER: _ClassVar[int] + pageUrl: str + tree: str + hash: str + image: str + pageLoadTime: str + keyId: str + test: bool + issueType: str + notes: str + def __init__(self, pageUrl: _Optional[str] = ..., tree: _Optional[str] = ..., hash: _Optional[str] = ..., image: _Optional[str] = ..., pageLoadTime: _Optional[str] = ..., keyId: _Optional[str] = ..., test: bool = ..., issueType: _Optional[str] = ..., notes: _Optional[str] = ...) -> None: ... + +class CustomerCaptureResponse(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class Error(_message.Message): + __slots__ = ("code", "message", "extras") + class ExtrasEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + CODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + EXTRAS_FIELD_NUMBER: _ClassVar[int] + code: str + message: str + extras: _containers.ScalarMap[str, str] + def __init__(self, code: _Optional[str] = ..., message: _Optional[str] = ..., extras: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class QuotePurchase(_message.Message): + __slots__ = ("quoteTotal", "includedTax", "includedOtherAddons", "taxAmount", "taxLabel") + QUOTETOTAL_FIELD_NUMBER: _ClassVar[int] + INCLUDEDTAX_FIELD_NUMBER: _ClassVar[int] + INCLUDEDOTHERADDONS_FIELD_NUMBER: _ClassVar[int] + TAXAMOUNT_FIELD_NUMBER: _ClassVar[int] + TAXLABEL_FIELD_NUMBER: _ClassVar[int] + quoteTotal: float + includedTax: bool + includedOtherAddons: bool + taxAmount: float + taxLabel: str + def __init__(self, quoteTotal: _Optional[float] = ..., includedTax: bool = ..., includedOtherAddons: bool = ..., taxAmount: _Optional[float] = ..., taxLabel: _Optional[str] = ...) -> None: ... + +class UpgradeLicenseStatusRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class UpgradeLicenseStatusResponse(_message.Message): + __slots__ = ("allowPurchaseFromConsole", "checkoutLink", "error") + ALLOWPURCHASEFROMCONSOLE_FIELD_NUMBER: _ClassVar[int] + CHECKOUTLINK_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + allowPurchaseFromConsole: bool + checkoutLink: str + error: Error + def __init__(self, allowPurchaseFromConsole: bool = ..., checkoutLink: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... + +class UpgradeLicenseQuotePurchaseRequest(_message.Message): + __slots__ = ("productType", "quantity") + PRODUCTTYPE_FIELD_NUMBER: _ClassVar[int] + QUANTITY_FIELD_NUMBER: _ClassVar[int] + productType: PurchaseProductType + quantity: int + def __init__(self, productType: _Optional[_Union[PurchaseProductType, str]] = ..., quantity: _Optional[int] = ...) -> None: ... + +class UpgradeLicenseQuotePurchaseResponse(_message.Message): + __slots__ = ("success", "quotePurchase", "viewSummaryLink", "error") + SUCCESS_FIELD_NUMBER: _ClassVar[int] + QUOTEPURCHASE_FIELD_NUMBER: _ClassVar[int] + VIEWSUMMARYLINK_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + success: bool + quotePurchase: QuotePurchase + viewSummaryLink: str + error: Error + def __init__(self, success: bool = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ..., viewSummaryLink: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... + +class UpgradeLicenseCompletePurchaseRequest(_message.Message): + __slots__ = ("productType", "quantity", "quotePurchase") + PRODUCTTYPE_FIELD_NUMBER: _ClassVar[int] + QUANTITY_FIELD_NUMBER: _ClassVar[int] + QUOTEPURCHASE_FIELD_NUMBER: _ClassVar[int] + productType: PurchaseProductType + quantity: int + quotePurchase: QuotePurchase + def __init__(self, productType: _Optional[_Union[PurchaseProductType, str]] = ..., quantity: _Optional[int] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ...) -> None: ... + +class UpgradeLicenseCompletePurchaseResponse(_message.Message): + __slots__ = ("success", "invoiceNumber", "error", "quotePurchase") + SUCCESS_FIELD_NUMBER: _ClassVar[int] + INVOICENUMBER_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + QUOTEPURCHASE_FIELD_NUMBER: _ClassVar[int] + success: bool + invoiceNumber: str + error: Error + quotePurchase: QuotePurchase + def __init__(self, success: bool = ..., invoiceNumber: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ...) -> None: ... + +class EnterpriseBasePlan(_message.Message): + __slots__ = ("baseplanVersion", "cost") + class EnterpriseBasePlanVersion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UNKNOWN: _ClassVar[EnterpriseBasePlan.EnterpriseBasePlanVersion] + BUSINESS_STARTER: _ClassVar[EnterpriseBasePlan.EnterpriseBasePlanVersion] + BUSINESS: _ClassVar[EnterpriseBasePlan.EnterpriseBasePlanVersion] + ENTERPRISE: _ClassVar[EnterpriseBasePlan.EnterpriseBasePlanVersion] + UNKNOWN: EnterpriseBasePlan.EnterpriseBasePlanVersion + BUSINESS_STARTER: EnterpriseBasePlan.EnterpriseBasePlanVersion + BUSINESS: EnterpriseBasePlan.EnterpriseBasePlanVersion + ENTERPRISE: EnterpriseBasePlan.EnterpriseBasePlanVersion + BASEPLANVERSION_FIELD_NUMBER: _ClassVar[int] + COST_FIELD_NUMBER: _ClassVar[int] + baseplanVersion: EnterpriseBasePlan.EnterpriseBasePlanVersion + cost: Cost + def __init__(self, baseplanVersion: _Optional[_Union[EnterpriseBasePlan.EnterpriseBasePlanVersion, str]] = ..., cost: _Optional[_Union[Cost, _Mapping]] = ...) -> None: ... + +class SubscriptionEnterprisePricingRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscriptionEnterprisePricingResponse(_message.Message): + __slots__ = ("basePlans", "addons", "filePlans") + BASEPLANS_FIELD_NUMBER: _ClassVar[int] + ADDONS_FIELD_NUMBER: _ClassVar[int] + FILEPLANS_FIELD_NUMBER: _ClassVar[int] + basePlans: _containers.RepeatedCompositeFieldContainer[EnterpriseBasePlan] + addons: _containers.RepeatedCompositeFieldContainer[Addon] + filePlans: _containers.RepeatedCompositeFieldContainer[FilePlan] + def __init__(self, basePlans: _Optional[_Iterable[_Union[EnterpriseBasePlan, _Mapping]]] = ..., addons: _Optional[_Iterable[_Union[Addon, _Mapping]]] = ..., filePlans: _Optional[_Iterable[_Union[FilePlan, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py index 3d982c22..1857c268 100644 --- a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: GraphSync.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'GraphSync.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -24,7 +16,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fGraphSync.proto\x12\tGraphSync\"M\n\x0cGraphSyncRef\x12 \n\x04type\x18\x01 \x01(\x0e\x32\x12.GraphSync.RefType\x12\r\n\x05value\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\"p\n\x0eGraphSyncActor\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.GraphSync.GraphSyncActorType\x12\n\n\x02id\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x17\n\x0f\x65\x66\x66\x65\x63tiveUserId\x18\x04 \x01(\x0c\"\xac\x01\n\rGraphSyncData\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.GraphSync.GraphSyncDataType\x12$\n\x03ref\x18\x02 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12*\n\tparentRef\x18\x03 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\x0c\x12\x0c\n\x04path\x18\x05 \x01(\t\"x\n\x11GraphSyncDataPlus\x12&\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x18.GraphSync.GraphSyncData\x12\x11\n\ttimestamp\x18\x02 \x01(\x03\x12(\n\x05\x61\x63tor\x18\x03 \x01(\x0b\x32\x19.GraphSync.GraphSyncActor\"W\n\x0eGraphSyncQuery\x12\x10\n\x08streamId\x18\x01 \x01(\x0c\x12\x0e\n\x06origin\x18\x02 \x01(\x0c\x12\x11\n\tsyncPoint\x18\x03 \x01(\x03\x12\x10\n\x08maxCount\x18\x04 \x01(\x05\"s\n\x0fGraphSyncResult\x12\x10\n\x08streamId\x18\x02 \x01(\x0c\x12\x11\n\tsyncPoint\x18\x03 \x01(\x03\x12*\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x1c.GraphSync.GraphSyncDataPlus\x12\x0f\n\x07hasMore\x18\x05 \x01(\x08\"A\n\x13GraphSyncMultiQuery\x12*\n\x07queries\x18\x01 \x03(\x0b\x32\x19.GraphSync.GraphSyncQuery\"C\n\x14GraphSyncMultiResult\x12+\n\x07results\x18\x01 \x03(\x0b\x32\x1a.GraphSync.GraphSyncResult\"j\n\x17GraphSyncAddDataRequest\x12\'\n\x06origin\x18\x01 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12&\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x18.GraphSync.GraphSyncData\"\'\n\x13GraphSyncLeafsQuery\x12\x10\n\x08vertices\x18\x02 \x03(\x0c\"<\n\x13GraphSyncRefsResult\x12%\n\x04refs\x18\x01 \x03(\x0b\x32\x17.GraphSync.GraphSyncRef*\xb2\x02\n\x07RefType\x12\x13\n\x0fRFT_UNSPECIFIED\x10\x00\x12\x0f\n\x0bRFT_GENERAL\x10\x01\x12\x08\n\x04USER\x10\x02\x12\n\n\x06\x44\x45VICE\x10\x03\x12\x07\n\x03REC\x10\x04\x12\n\n\x06\x46OLDER\x10\x05\x12\x08\n\x04TEAM\x10\x06\x12\x0e\n\nENTERPRISE\x10\x07\x12\x11\n\rPAM_DIRECTORY\x10\x08\x12\x0f\n\x0bPAM_MACHINE\x10\t\x12\x10\n\x0cPAM_DATABASE\x10\n\x12\x0c\n\x08PAM_USER\x10\x0b\x12\x0f\n\x0bPAM_NETWORK\x10\x0c\x12\x0f\n\x0bPAM_BROWSER\x10\r\x12\x0e\n\nCONNECTION\x10\x0e\x12\x0c\n\x08WORKFLOW\x10\x0f\x12\x10\n\x0cNOTIFICATION\x10\x10\x12\r\n\tUSER_INFO\x10\x11\x12\r\n\tTEAM_INFO\x10\x12\x12\x08\n\x04ROLE\x10\x13*p\n\x11GraphSyncDataType\x12\x13\n\x0fGSE_UNSPECIFIED\x10\x00\x12\x0c\n\x08GSE_DATA\x10\x01\x12\x0b\n\x07GSE_KEY\x10\x02\x12\x0c\n\x08GSE_LINK\x10\x03\x12\x0b\n\x07GSE_ACL\x10\x04\x12\x10\n\x0cGSE_DELETION\x10\x05*]\n\x12GraphSyncActorType\x12\x13\n\x0fGSA_UNSPECIFIED\x10\x00\x12\x0c\n\x08GSA_USER\x10\x01\x12\x0f\n\x0bGSA_SERVICE\x10\x02\x12\x13\n\x0fGSA_PAM_GATEWAY\x10\x03\x42%\n\x18\x63om.keepersecurity.protoB\tGraphSyncb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fGraphSync.proto\x12\tGraphSync\"M\n\x0cGraphSyncRef\x12 \n\x04type\x18\x01 \x01(\x0e\x32\x12.GraphSync.RefType\x12\r\n\x05value\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\"p\n\x0eGraphSyncActor\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.GraphSync.GraphSyncActorType\x12\n\n\x02id\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x17\n\x0f\x65\x66\x66\x65\x63tiveUserId\x18\x04 \x01(\x0c\"\xac\x01\n\rGraphSyncData\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.GraphSync.GraphSyncDataType\x12$\n\x03ref\x18\x02 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12*\n\tparentRef\x18\x03 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\x0c\x12\x0c\n\x04path\x18\x05 \x01(\t\"x\n\x11GraphSyncDataPlus\x12&\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x18.GraphSync.GraphSyncData\x12\x11\n\ttimestamp\x18\x02 \x01(\x03\x12(\n\x05\x61\x63tor\x18\x03 \x01(\x0b\x32\x19.GraphSync.GraphSyncActor\"W\n\x0eGraphSyncQuery\x12\x10\n\x08streamId\x18\x01 \x01(\x0c\x12\x0e\n\x06origin\x18\x02 \x01(\x0c\x12\x11\n\tsyncPoint\x18\x03 \x01(\x03\x12\x10\n\x08maxCount\x18\x04 \x01(\x05\"s\n\x0fGraphSyncResult\x12\x10\n\x08streamId\x18\x02 \x01(\x0c\x12\x11\n\tsyncPoint\x18\x03 \x01(\x03\x12*\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x1c.GraphSync.GraphSyncDataPlus\x12\x0f\n\x07hasMore\x18\x05 \x01(\x08\"A\n\x13GraphSyncMultiQuery\x12*\n\x07queries\x18\x01 \x03(\x0b\x32\x19.GraphSync.GraphSyncQuery\"C\n\x14GraphSyncMultiResult\x12+\n\x07results\x18\x01 \x03(\x0b\x32\x1a.GraphSync.GraphSyncResult\"j\n\x17GraphSyncAddDataRequest\x12\'\n\x06origin\x18\x01 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12&\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x18.GraphSync.GraphSyncData\"\'\n\x13GraphSyncLeafsQuery\x12\x10\n\x08vertices\x18\x02 \x03(\x0c\"<\n\x13GraphSyncRefsResult\x12%\n\x04refs\x18\x01 \x03(\x0b\x32\x17.GraphSync.GraphSyncRef*\xe5\x02\n\x07RefType\x12\x0f\n\x0bRFT_GENERAL\x10\x00\x12\x0c\n\x08RFT_USER\x10\x01\x12\x0e\n\nRFT_DEVICE\x10\x02\x12\x0b\n\x07RFT_REC\x10\x03\x12\x0e\n\nRFT_FOLDER\x10\x04\x12\x0c\n\x08RFT_TEAM\x10\x05\x12\x12\n\x0eRFT_ENTERPRISE\x10\x06\x12\x15\n\x11RFT_PAM_DIRECTORY\x10\x07\x12\x13\n\x0fRFT_PAM_MACHINE\x10\x08\x12\x14\n\x10RFT_PAM_DATABASE\x10\t\x12\x10\n\x0cRFT_PAM_USER\x10\n\x12\x13\n\x0fRFT_PAM_NETWORK\x10\x0b\x12\x13\n\x0fRFT_PAM_BROWSER\x10\x0c\x12\x12\n\x0eRFT_CONNECTION\x10\r\x12\x10\n\x0cRFT_WORKFLOW\x10\x0e\x12\x14\n\x10RFT_NOTIFICATION\x10\x0f\x12\x11\n\rRFT_USER_INFO\x10\x10\x12\x11\n\rRFT_TEAM_INFO\x10\x11\x12\x0c\n\x08RFT_ROLE\x10\x12*[\n\x11GraphSyncDataType\x12\x0c\n\x08GSE_DATA\x10\x00\x12\x0b\n\x07GSE_KEY\x10\x01\x12\x0c\n\x08GSE_LINK\x10\x02\x12\x0b\n\x07GSE_ACL\x10\x03\x12\x10\n\x0cGSE_DELETION\x10\x04*H\n\x12GraphSyncActorType\x12\x0c\n\x08GSA_USER\x10\x00\x12\x0f\n\x0bGSA_SERVICE\x10\x01\x12\x13\n\x0fGSA_PAM_GATEWAY\x10\x02\x42%\n\x18\x63om.keepersecurity.protoB\tGraphSyncb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,11 +25,11 @@ _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\tGraphSync' _globals['_REFTYPE']._serialized_start=1074 - _globals['_REFTYPE']._serialized_end=1380 - _globals['_GRAPHSYNCDATATYPE']._serialized_start=1382 - _globals['_GRAPHSYNCDATATYPE']._serialized_end=1494 - _globals['_GRAPHSYNCACTORTYPE']._serialized_start=1496 - _globals['_GRAPHSYNCACTORTYPE']._serialized_end=1589 + _globals['_REFTYPE']._serialized_end=1431 + _globals['_GRAPHSYNCDATATYPE']._serialized_start=1433 + _globals['_GRAPHSYNCDATATYPE']._serialized_end=1524 + _globals['_GRAPHSYNCACTORTYPE']._serialized_start=1526 + _globals['_GRAPHSYNCACTORTYPE']._serialized_end=1598 _globals['_GRAPHSYNCREF']._serialized_start=30 _globals['_GRAPHSYNCREF']._serialized_end=107 _globals['_GRAPHSYNCACTOR']._serialized_start=109 diff --git a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi index c128104a..594a4eec 100644 --- a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi @@ -8,30 +8,28 @@ DESCRIPTOR: _descriptor.FileDescriptor class RefType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () - RFT_UNSPECIFIED: _ClassVar[RefType] RFT_GENERAL: _ClassVar[RefType] - USER: _ClassVar[RefType] - DEVICE: _ClassVar[RefType] - REC: _ClassVar[RefType] - FOLDER: _ClassVar[RefType] - TEAM: _ClassVar[RefType] - ENTERPRISE: _ClassVar[RefType] - PAM_DIRECTORY: _ClassVar[RefType] - PAM_MACHINE: _ClassVar[RefType] - PAM_DATABASE: _ClassVar[RefType] - PAM_USER: _ClassVar[RefType] - PAM_NETWORK: _ClassVar[RefType] - PAM_BROWSER: _ClassVar[RefType] - CONNECTION: _ClassVar[RefType] - WORKFLOW: _ClassVar[RefType] - NOTIFICATION: _ClassVar[RefType] - USER_INFO: _ClassVar[RefType] - TEAM_INFO: _ClassVar[RefType] - ROLE: _ClassVar[RefType] + RFT_USER: _ClassVar[RefType] + RFT_DEVICE: _ClassVar[RefType] + RFT_REC: _ClassVar[RefType] + RFT_FOLDER: _ClassVar[RefType] + RFT_TEAM: _ClassVar[RefType] + RFT_ENTERPRISE: _ClassVar[RefType] + RFT_PAM_DIRECTORY: _ClassVar[RefType] + RFT_PAM_MACHINE: _ClassVar[RefType] + RFT_PAM_DATABASE: _ClassVar[RefType] + RFT_PAM_USER: _ClassVar[RefType] + RFT_PAM_NETWORK: _ClassVar[RefType] + RFT_PAM_BROWSER: _ClassVar[RefType] + RFT_CONNECTION: _ClassVar[RefType] + RFT_WORKFLOW: _ClassVar[RefType] + RFT_NOTIFICATION: _ClassVar[RefType] + RFT_USER_INFO: _ClassVar[RefType] + RFT_TEAM_INFO: _ClassVar[RefType] + RFT_ROLE: _ClassVar[RefType] class GraphSyncDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () - GSE_UNSPECIFIED: _ClassVar[GraphSyncDataType] GSE_DATA: _ClassVar[GraphSyncDataType] GSE_KEY: _ClassVar[GraphSyncDataType] GSE_LINK: _ClassVar[GraphSyncDataType] @@ -40,37 +38,33 @@ class GraphSyncDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): class GraphSyncActorType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () - GSA_UNSPECIFIED: _ClassVar[GraphSyncActorType] GSA_USER: _ClassVar[GraphSyncActorType] GSA_SERVICE: _ClassVar[GraphSyncActorType] GSA_PAM_GATEWAY: _ClassVar[GraphSyncActorType] -RFT_UNSPECIFIED: RefType RFT_GENERAL: RefType -USER: RefType -DEVICE: RefType -REC: RefType -FOLDER: RefType -TEAM: RefType -ENTERPRISE: RefType -PAM_DIRECTORY: RefType -PAM_MACHINE: RefType -PAM_DATABASE: RefType -PAM_USER: RefType -PAM_NETWORK: RefType -PAM_BROWSER: RefType -CONNECTION: RefType -WORKFLOW: RefType -NOTIFICATION: RefType -USER_INFO: RefType -TEAM_INFO: RefType -ROLE: RefType -GSE_UNSPECIFIED: GraphSyncDataType +RFT_USER: RefType +RFT_DEVICE: RefType +RFT_REC: RefType +RFT_FOLDER: RefType +RFT_TEAM: RefType +RFT_ENTERPRISE: RefType +RFT_PAM_DIRECTORY: RefType +RFT_PAM_MACHINE: RefType +RFT_PAM_DATABASE: RefType +RFT_PAM_USER: RefType +RFT_PAM_NETWORK: RefType +RFT_PAM_BROWSER: RefType +RFT_CONNECTION: RefType +RFT_WORKFLOW: RefType +RFT_NOTIFICATION: RefType +RFT_USER_INFO: RefType +RFT_TEAM_INFO: RefType +RFT_ROLE: RefType GSE_DATA: GraphSyncDataType GSE_KEY: GraphSyncDataType GSE_LINK: GraphSyncDataType GSE_ACL: GraphSyncDataType GSE_DELETION: GraphSyncDataType -GSA_UNSPECIFIED: GraphSyncActorType GSA_USER: GraphSyncActorType GSA_SERVICE: GraphSyncActorType GSA_PAM_GATEWAY: GraphSyncActorType diff --git a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py index 3b3f2a4c..111c616d 100644 --- a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: NotificationCenter.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'NotificationCenter.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -25,7 +17,7 @@ from . import GraphSync_pb2 as GraphSync__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18NotificationCenter.proto\x12\x12NotificationCenter\x1a\x0fGraphSync.proto\".\n\rEncryptedData\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xa0\x02\n\x0cNotification\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.NotificationCenter.NotificationType\x12:\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32(.NotificationCenter.NotificationCategory\x12\'\n\x06sender\x18\x03 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12\x16\n\x0esenderFullName\x18\x04 \x01(\t\x12\x38\n\rencryptedData\x18\x05 \x01(\x0b\x32!.NotificationCenter.EncryptedData\x12%\n\x04refs\x18\x06 \x03(\x0b\x32\x17.GraphSync.GraphSyncRef\"\x97\x01\n\x14NotificationReadMark\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x1c\n\x14notification_edge_id\x18\x02 \x01(\x03\x12\x14\n\x0cmark_edge_id\x18\x03 \x01(\x03\x12>\n\nreadStatus\x18\x04 \x01(\x0e\x32*.NotificationCenter.NotificationReadStatus\"\x8d\x02\n\x13NotificationContent\x12\x38\n\x0cnotification\x18\x01 \x01(\x0b\x32 .NotificationCenter.NotificationH\x00\x12@\n\nreadStatus\x18\x02 \x01(\x0e\x32*.NotificationCenter.NotificationReadStatusH\x00\x12H\n\x0e\x61pprovalStatus\x18\x03 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatusH\x00\x12\x15\n\rclientTypeIDs\x18\x04 \x03(\x05\x12\x11\n\tdeviceIDs\x18\x05 \x03(\x03\x42\x06\n\x04type\"o\n\x13NotificationWrapper\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x38\n\x07\x63ontent\x18\x02 \x01(\x0b\x32\'.NotificationCenter.NotificationContent\x12\x11\n\ttimestamp\x18\x03 \x01(\x03\"m\n\x10NotificationSync\x12\x35\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\'.NotificationCenter.NotificationWrapper\x12\x11\n\tsyncPoint\x18\x02 \x01(\x03\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\"g\n\x10ReadStatusUpdate\x12\x17\n\x0fnotificationUid\x18\x01 \x01(\x0c\x12:\n\x06status\x18\x02 \x01(\x0e\x32*.NotificationCenter.NotificationReadStatus\"o\n\x14\x41pprovalStatusUpdate\x12\x17\n\x0fnotificationUid\x18\x01 \x01(\x0c\x12>\n\x06status\x18\x02 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\"^\n\x1cProcessMarkReadEventsRequest\x12>\n\x10readStatusUpdate\x18\x01 \x03(\x0b\x32$.NotificationCenter.ReadStatusUpdate\"\xa8\x01\n\x17NotificationSendRequest\x12+\n\nrecipients\x18\x01 \x03(\x0b\x32\x17.GraphSync.GraphSyncRef\x12\x36\n\x0cnotification\x18\x02 \x01(\x0b\x32 .NotificationCenter.Notification\x12\x15\n\rclientTypeIDs\x18\x03 \x03(\x05\x12\x11\n\tdeviceIDs\x18\x04 \x03(\x03\"^\n\x18NotificationsSendRequest\x12\x42\n\rnotifications\x18\x01 \x03(\x0b\x32+.NotificationCenter.NotificationSendRequest\",\n\x17NotificationSyncRequest\x12\x11\n\tsyncPoint\x18\x01 \x01(\x03*]\n\x14NotificationCategory\x12\x12\n\x0eNC_UNSPECIFIED\x10\x00\x12\x0e\n\nNC_ACCOUNT\x10\x01\x12\x0e\n\nNC_SHARING\x10\x02\x12\x11\n\rNC_ENTERPRISE\x10\x03*\xab\x02\n\x10NotificationType\x12\x12\n\x0eNT_UNSPECIFIED\x10\x00\x12\x0c\n\x08NT_ALERT\x10\x01\x12\x16\n\x12NT_DEVICE_APPROVAL\x10\x02\x12\x1a\n\x16NT_MASTER_PASS_UPDATED\x10\x03\x12\x15\n\x11NT_SHARE_APPROVAL\x10\x04\x12\x1e\n\x1aNT_SHARE_APPROVAL_APPROVED\x10\x05\x12\r\n\tNT_SHARED\x10\x06\x12\x12\n\x0eNT_TRANSFERRED\x10\x07\x12\x1c\n\x18NT_LICENSE_LIMIT_REACHED\x10\x08\x12\x17\n\x13NT_APPROVAL_REQUEST\x10\t\x12\x18\n\x14NT_APPROVED_RESPONSE\x10\n\x12\x16\n\x12NT_DENIED_RESPONSE\x10\x0b*Y\n\x16NotificationReadStatus\x12\x13\n\x0fNRS_UNSPECIFIED\x10\x00\x12\x0c\n\x08NRS_LAST\x10\x01\x12\x0c\n\x08NRS_READ\x10\x02\x12\x0e\n\nNRS_UNREAD\x10\x03*\x86\x01\n\x1aNotificationApprovalStatus\x12\x13\n\x0fNAS_UNSPECIFIED\x10\x00\x12\x10\n\x0cNAS_APPROVED\x10\x01\x12\x0e\n\nNAS_DENIED\x10\x02\x12\x1c\n\x18NAS_LOST_APPROVAL_RIGHTS\x10\x03\x12\x13\n\x0fNAS_LOST_ACCESS\x10\x04\x42.\n\x18\x63om.keepersecurity.protoB\x12NotificationCenterb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18NotificationCenter.proto\x12\x12NotificationCenter\x1a\x0fGraphSync.proto\".\n\rEncryptedData\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xe2\x02\n\x0cNotification\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.NotificationCenter.NotificationType\x12>\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32(.NotificationCenter.NotificationCategoryB\x02\x18\x01\x12\'\n\x06sender\x18\x03 \x01(\x0b\x32\x17.GraphSync.GraphSyncRef\x12\x16\n\x0esenderFullName\x18\x04 \x01(\t\x12\x38\n\rencryptedData\x18\x05 \x01(\x0b\x32!.NotificationCenter.EncryptedData\x12%\n\x04refs\x18\x06 \x03(\x0b\x32\x17.GraphSync.GraphSyncRef\x12<\n\ncategories\x18\x07 \x03(\x0e\x32(.NotificationCenter.NotificationCategory\"\x97\x01\n\x14NotificationReadMark\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x1c\n\x14notification_edge_id\x18\x02 \x01(\x03\x12\x14\n\x0cmark_edge_id\x18\x03 \x01(\x03\x12>\n\nreadStatus\x18\x04 \x01(\x0e\x32*.NotificationCenter.NotificationReadStatus\"\xa6\x02\n\x13NotificationContent\x12\x38\n\x0cnotification\x18\x01 \x01(\x0b\x32 .NotificationCenter.NotificationH\x00\x12@\n\nreadStatus\x18\x02 \x01(\x0e\x32*.NotificationCenter.NotificationReadStatusH\x00\x12H\n\x0e\x61pprovalStatus\x18\x03 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatusH\x00\x12\x17\n\rtrimmingPoint\x18\x04 \x01(\x08H\x00\x12\x15\n\rclientTypeIDs\x18\x05 \x03(\x05\x12\x11\n\tdeviceIDs\x18\x06 \x03(\x03\x42\x06\n\x04type\"o\n\x13NotificationWrapper\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x38\n\x07\x63ontent\x18\x02 \x01(\x0b\x32\'.NotificationCenter.NotificationContent\x12\x11\n\ttimestamp\x18\x03 \x01(\x03\"m\n\x10NotificationSync\x12\x35\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\'.NotificationCenter.NotificationWrapper\x12\x11\n\tsyncPoint\x18\x02 \x01(\x03\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\"g\n\x10ReadStatusUpdate\x12\x17\n\x0fnotificationUid\x18\x01 \x01(\x0c\x12:\n\x06status\x18\x02 \x01(\x0e\x32*.NotificationCenter.NotificationReadStatus\"o\n\x14\x41pprovalStatusUpdate\x12\x17\n\x0fnotificationUid\x18\x01 \x01(\x0c\x12>\n\x06status\x18\x02 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\"^\n\x1cProcessMarkReadEventsRequest\x12>\n\x10readStatusUpdate\x18\x01 \x03(\x0b\x32$.NotificationCenter.ReadStatusUpdate\"\xa8\x01\n\x17NotificationSendRequest\x12+\n\nrecipients\x18\x01 \x03(\x0b\x32\x17.GraphSync.GraphSyncRef\x12\x36\n\x0cnotification\x18\x02 \x01(\x0b\x32 .NotificationCenter.Notification\x12\x15\n\rclientTypeIDs\x18\x03 \x03(\x05\x12\x11\n\tdeviceIDs\x18\x04 \x03(\x03\"^\n\x18NotificationsSendRequest\x12\x42\n\rnotifications\x18\x01 \x03(\x0b\x32+.NotificationCenter.NotificationSendRequest\",\n\x17NotificationSyncRequest\x12\x11\n\tsyncPoint\x18\x01 \x01(\x03*\x9f\x01\n\x14NotificationCategory\x12\x12\n\x0eNC_UNSPECIFIED\x10\x00\x12\x0e\n\nNC_ACCOUNT\x10\x01\x12\x0e\n\nNC_SHARING\x10\x02\x12\x11\n\rNC_ENTERPRISE\x10\x03\x12\x0f\n\x0bNC_SECURITY\x10\x04\x12\x0e\n\nNC_REQUEST\x10\x05\x12\r\n\tNC_SYSTEM\x10\x06\x12\x10\n\x0cNC_PROMOTION\x10\x07*\xe0\x02\n\x10NotificationType\x12\x12\n\x0eNT_UNSPECIFIED\x10\x00\x12\x0c\n\x08NT_ALERT\x10\x01\x12\x16\n\x12NT_DEVICE_APPROVAL\x10\x02\x12\x1a\n\x16NT_MASTER_PASS_UPDATED\x10\x03\x12\x15\n\x11NT_SHARE_APPROVAL\x10\x04\x12\x1e\n\x1aNT_SHARE_APPROVAL_APPROVED\x10\x05\x12\r\n\tNT_SHARED\x10\x06\x12\x12\n\x0eNT_TRANSFERRED\x10\x07\x12\x1c\n\x18NT_LICENSE_LIMIT_REACHED\x10\x08\x12\x17\n\x13NT_APPROVAL_REQUEST\x10\t\x12\x18\n\x14NT_APPROVED_RESPONSE\x10\n\x12\x16\n\x12NT_DENIED_RESPONSE\x10\x0b\x12\x15\n\x11NT_2FA_CONFIGURED\x10\x0c\x12\x1c\n\x18NT_SHARE_APPROVAL_DENIED\x10\r*Y\n\x16NotificationReadStatus\x12\x13\n\x0fNRS_UNSPECIFIED\x10\x00\x12\x0c\n\x08NRS_LAST\x10\x01\x12\x0c\n\x08NRS_READ\x10\x02\x12\x0e\n\nNRS_UNREAD\x10\x03*\x86\x01\n\x1aNotificationApprovalStatus\x12\x13\n\x0fNAS_UNSPECIFIED\x10\x00\x12\x10\n\x0cNAS_APPROVED\x10\x01\x12\x0e\n\nNAS_DENIED\x10\x02\x12\x1c\n\x18NAS_LOST_APPROVAL_RIGHTS\x10\x03\x12\x13\n\x0fNAS_LOST_ACCESS\x10\x04\x42.\n\x18\x63om.keepersecurity.protoB\x12NotificationCenterb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,36 +25,38 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\022NotificationCenter' - _globals['_NOTIFICATIONCATEGORY']._serialized_start=1681 - _globals['_NOTIFICATIONCATEGORY']._serialized_end=1774 - _globals['_NOTIFICATIONTYPE']._serialized_start=1777 - _globals['_NOTIFICATIONTYPE']._serialized_end=2076 - _globals['_NOTIFICATIONREADSTATUS']._serialized_start=2078 - _globals['_NOTIFICATIONREADSTATUS']._serialized_end=2167 - _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_start=2170 - _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_end=2304 + _globals['_NOTIFICATION'].fields_by_name['category']._loaded_options = None + _globals['_NOTIFICATION'].fields_by_name['category']._serialized_options = b'\030\001' + _globals['_NOTIFICATIONCATEGORY']._serialized_start=1773 + _globals['_NOTIFICATIONCATEGORY']._serialized_end=1932 + _globals['_NOTIFICATIONTYPE']._serialized_start=1935 + _globals['_NOTIFICATIONTYPE']._serialized_end=2287 + _globals['_NOTIFICATIONREADSTATUS']._serialized_start=2289 + _globals['_NOTIFICATIONREADSTATUS']._serialized_end=2378 + _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_start=2381 + _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_end=2515 _globals['_ENCRYPTEDDATA']._serialized_start=65 _globals['_ENCRYPTEDDATA']._serialized_end=111 _globals['_NOTIFICATION']._serialized_start=114 - _globals['_NOTIFICATION']._serialized_end=402 - _globals['_NOTIFICATIONREADMARK']._serialized_start=405 - _globals['_NOTIFICATIONREADMARK']._serialized_end=556 - _globals['_NOTIFICATIONCONTENT']._serialized_start=559 - _globals['_NOTIFICATIONCONTENT']._serialized_end=828 - _globals['_NOTIFICATIONWRAPPER']._serialized_start=830 - _globals['_NOTIFICATIONWRAPPER']._serialized_end=941 - _globals['_NOTIFICATIONSYNC']._serialized_start=943 - _globals['_NOTIFICATIONSYNC']._serialized_end=1052 - _globals['_READSTATUSUPDATE']._serialized_start=1054 - _globals['_READSTATUSUPDATE']._serialized_end=1157 - _globals['_APPROVALSTATUSUPDATE']._serialized_start=1159 - _globals['_APPROVALSTATUSUPDATE']._serialized_end=1270 - _globals['_PROCESSMARKREADEVENTSREQUEST']._serialized_start=1272 - _globals['_PROCESSMARKREADEVENTSREQUEST']._serialized_end=1366 - _globals['_NOTIFICATIONSENDREQUEST']._serialized_start=1369 - _globals['_NOTIFICATIONSENDREQUEST']._serialized_end=1537 - _globals['_NOTIFICATIONSSENDREQUEST']._serialized_start=1539 - _globals['_NOTIFICATIONSSENDREQUEST']._serialized_end=1633 - _globals['_NOTIFICATIONSYNCREQUEST']._serialized_start=1635 - _globals['_NOTIFICATIONSYNCREQUEST']._serialized_end=1679 + _globals['_NOTIFICATION']._serialized_end=468 + _globals['_NOTIFICATIONREADMARK']._serialized_start=471 + _globals['_NOTIFICATIONREADMARK']._serialized_end=622 + _globals['_NOTIFICATIONCONTENT']._serialized_start=625 + _globals['_NOTIFICATIONCONTENT']._serialized_end=919 + _globals['_NOTIFICATIONWRAPPER']._serialized_start=921 + _globals['_NOTIFICATIONWRAPPER']._serialized_end=1032 + _globals['_NOTIFICATIONSYNC']._serialized_start=1034 + _globals['_NOTIFICATIONSYNC']._serialized_end=1143 + _globals['_READSTATUSUPDATE']._serialized_start=1145 + _globals['_READSTATUSUPDATE']._serialized_end=1248 + _globals['_APPROVALSTATUSUPDATE']._serialized_start=1250 + _globals['_APPROVALSTATUSUPDATE']._serialized_end=1361 + _globals['_PROCESSMARKREADEVENTSREQUEST']._serialized_start=1363 + _globals['_PROCESSMARKREADEVENTSREQUEST']._serialized_end=1457 + _globals['_NOTIFICATIONSENDREQUEST']._serialized_start=1460 + _globals['_NOTIFICATIONSENDREQUEST']._serialized_end=1628 + _globals['_NOTIFICATIONSSENDREQUEST']._serialized_start=1630 + _globals['_NOTIFICATIONSSENDREQUEST']._serialized_end=1724 + _globals['_NOTIFICATIONSYNCREQUEST']._serialized_start=1726 + _globals['_NOTIFICATIONSYNCREQUEST']._serialized_end=1770 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi index 4aaa489e..78c9948a 100644 --- a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi @@ -13,6 +13,10 @@ class NotificationCategory(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): NC_ACCOUNT: _ClassVar[NotificationCategory] NC_SHARING: _ClassVar[NotificationCategory] NC_ENTERPRISE: _ClassVar[NotificationCategory] + NC_SECURITY: _ClassVar[NotificationCategory] + NC_REQUEST: _ClassVar[NotificationCategory] + NC_SYSTEM: _ClassVar[NotificationCategory] + NC_PROMOTION: _ClassVar[NotificationCategory] class NotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -28,6 +32,8 @@ class NotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): NT_APPROVAL_REQUEST: _ClassVar[NotificationType] NT_APPROVED_RESPONSE: _ClassVar[NotificationType] NT_DENIED_RESPONSE: _ClassVar[NotificationType] + NT_2FA_CONFIGURED: _ClassVar[NotificationType] + NT_SHARE_APPROVAL_DENIED: _ClassVar[NotificationType] class NotificationReadStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -47,6 +53,10 @@ NC_UNSPECIFIED: NotificationCategory NC_ACCOUNT: NotificationCategory NC_SHARING: NotificationCategory NC_ENTERPRISE: NotificationCategory +NC_SECURITY: NotificationCategory +NC_REQUEST: NotificationCategory +NC_SYSTEM: NotificationCategory +NC_PROMOTION: NotificationCategory NT_UNSPECIFIED: NotificationType NT_ALERT: NotificationType NT_DEVICE_APPROVAL: NotificationType @@ -59,6 +69,8 @@ NT_LICENSE_LIMIT_REACHED: NotificationType NT_APPROVAL_REQUEST: NotificationType NT_APPROVED_RESPONSE: NotificationType NT_DENIED_RESPONSE: NotificationType +NT_2FA_CONFIGURED: NotificationType +NT_SHARE_APPROVAL_DENIED: NotificationType NRS_UNSPECIFIED: NotificationReadStatus NRS_LAST: NotificationReadStatus NRS_READ: NotificationReadStatus @@ -78,20 +90,22 @@ class EncryptedData(_message.Message): def __init__(self, version: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ... class Notification(_message.Message): - __slots__ = ("type", "category", "sender", "senderFullName", "encryptedData", "refs") + __slots__ = ("type", "category", "sender", "senderFullName", "encryptedData", "refs", "categories") TYPE_FIELD_NUMBER: _ClassVar[int] CATEGORY_FIELD_NUMBER: _ClassVar[int] SENDER_FIELD_NUMBER: _ClassVar[int] SENDERFULLNAME_FIELD_NUMBER: _ClassVar[int] ENCRYPTEDDATA_FIELD_NUMBER: _ClassVar[int] REFS_FIELD_NUMBER: _ClassVar[int] + CATEGORIES_FIELD_NUMBER: _ClassVar[int] type: NotificationType category: NotificationCategory sender: _GraphSync_pb2.GraphSyncRef senderFullName: str encryptedData: EncryptedData refs: _containers.RepeatedCompositeFieldContainer[_GraphSync_pb2.GraphSyncRef] - def __init__(self, type: _Optional[_Union[NotificationType, str]] = ..., category: _Optional[_Union[NotificationCategory, str]] = ..., sender: _Optional[_Union[_GraphSync_pb2.GraphSyncRef, _Mapping]] = ..., senderFullName: _Optional[str] = ..., encryptedData: _Optional[_Union[EncryptedData, _Mapping]] = ..., refs: _Optional[_Iterable[_Union[_GraphSync_pb2.GraphSyncRef, _Mapping]]] = ...) -> None: ... + categories: _containers.RepeatedScalarFieldContainer[NotificationCategory] + def __init__(self, type: _Optional[_Union[NotificationType, str]] = ..., category: _Optional[_Union[NotificationCategory, str]] = ..., sender: _Optional[_Union[_GraphSync_pb2.GraphSyncRef, _Mapping]] = ..., senderFullName: _Optional[str] = ..., encryptedData: _Optional[_Union[EncryptedData, _Mapping]] = ..., refs: _Optional[_Iterable[_Union[_GraphSync_pb2.GraphSyncRef, _Mapping]]] = ..., categories: _Optional[_Iterable[_Union[NotificationCategory, str]]] = ...) -> None: ... class NotificationReadMark(_message.Message): __slots__ = ("uid", "notification_edge_id", "mark_edge_id", "readStatus") @@ -106,18 +120,20 @@ class NotificationReadMark(_message.Message): def __init__(self, uid: _Optional[bytes] = ..., notification_edge_id: _Optional[int] = ..., mark_edge_id: _Optional[int] = ..., readStatus: _Optional[_Union[NotificationReadStatus, str]] = ...) -> None: ... class NotificationContent(_message.Message): - __slots__ = ("notification", "readStatus", "approvalStatus", "clientTypeIDs", "deviceIDs") + __slots__ = ("notification", "readStatus", "approvalStatus", "trimmingPoint", "clientTypeIDs", "deviceIDs") NOTIFICATION_FIELD_NUMBER: _ClassVar[int] READSTATUS_FIELD_NUMBER: _ClassVar[int] APPROVALSTATUS_FIELD_NUMBER: _ClassVar[int] + TRIMMINGPOINT_FIELD_NUMBER: _ClassVar[int] CLIENTTYPEIDS_FIELD_NUMBER: _ClassVar[int] DEVICEIDS_FIELD_NUMBER: _ClassVar[int] notification: Notification readStatus: NotificationReadStatus approvalStatus: NotificationApprovalStatus + trimmingPoint: bool clientTypeIDs: _containers.RepeatedScalarFieldContainer[int] deviceIDs: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, notification: _Optional[_Union[Notification, _Mapping]] = ..., readStatus: _Optional[_Union[NotificationReadStatus, str]] = ..., approvalStatus: _Optional[_Union[NotificationApprovalStatus, str]] = ..., clientTypeIDs: _Optional[_Iterable[int]] = ..., deviceIDs: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, notification: _Optional[_Union[Notification, _Mapping]] = ..., readStatus: _Optional[_Union[NotificationReadStatus, str]] = ..., approvalStatus: _Optional[_Union[NotificationApprovalStatus, str]] = ..., trimmingPoint: bool = ..., clientTypeIDs: _Optional[_Iterable[int]] = ..., deviceIDs: _Optional[_Iterable[int]] = ...) -> None: ... class NotificationWrapper(_message.Message): __slots__ = ("uid", "content", "timestamp") diff --git a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py index 3d0f6275..978c3cbb 100644 --- a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: SyncDown.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'SyncDown.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -29,7 +21,7 @@ from . import NotificationCenter_pb2 as NotificationCenter__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eSyncDown.proto\x12\x05Vault\x1a\x0crecord.proto\x1a\x11\x62reachwatch.proto\x1a\x10\x41PIRequest.proto\x1a\x10\x65nterprise.proto\x1a\x18NotificationCenter.proto\"A\n\x0fSyncDownRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x61taVersion\x18\x02 \x01(\x05\"\x81\x11\n\x10SyncDownResponse\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\x12\'\n\x0b\x63\x61\x63heStatus\x18\x03 \x01(\x0e\x32\x12.Vault.CacheStatus\x12&\n\x0buserFolders\x18\x04 \x03(\x0b\x32\x11.Vault.UserFolder\x12*\n\rsharedFolders\x18\x05 \x03(\x0b\x32\x13.Vault.SharedFolder\x12>\n\x17userFolderSharedFolders\x18\x06 \x03(\x0b\x32\x1d.Vault.UserFolderSharedFolder\x12\x36\n\x13sharedFolderFolders\x18\x07 \x03(\x0b\x32\x19.Vault.SharedFolderFolder\x12\x1e\n\x07records\x18\x08 \x03(\x0b\x32\r.Vault.Record\x12-\n\x0erecordMetaData\x18\t \x03(\x0b\x32\x15.Vault.RecordMetaData\x12+\n\rnonSharedData\x18\n \x03(\x0b\x32\x14.Vault.NonSharedData\x12&\n\x0brecordLinks\x18\x0b \x03(\x0b\x32\x11.Vault.RecordLink\x12\x32\n\x11userFolderRecords\x18\x0c \x03(\x0b\x32\x17.Vault.UserFolderRecord\x12\x36\n\x13sharedFolderRecords\x18\r \x03(\x0b\x32\x19.Vault.SharedFolderRecord\x12\x42\n\x19sharedFolderFolderRecords\x18\x0e \x03(\x0b\x32\x1f.Vault.SharedFolderFolderRecord\x12\x32\n\x11sharedFolderUsers\x18\x0f \x03(\x0b\x32\x17.Vault.SharedFolderUser\x12\x32\n\x11sharedFolderTeams\x18\x10 \x03(\x0b\x32\x17.Vault.SharedFolderTeam\x12\x1a\n\x12recordAddAuditData\x18\x11 \x03(\x0c\x12\x1a\n\x05teams\x18\x12 \x03(\x0b\x32\x0b.Vault.Team\x12,\n\x0esharingChanges\x18\x13 \x03(\x0b\x32\x14.Vault.SharingChange\x12\x1f\n\x07profile\x18\x14 \x01(\x0b\x32\x0e.Vault.Profile\x12%\n\nprofilePic\x18\x15 \x01(\x0b\x32\x11.Vault.ProfilePic\x12\x34\n\x12pendingTeamMembers\x18\x16 \x03(\x0b\x32\x18.Vault.PendingTeamMember\x12\x34\n\x12\x62reachWatchRecords\x18\x17 \x03(\x0b\x32\x18.Vault.BreachWatchRecord\x12\"\n\tuserAuths\x18\x18 \x03(\x0b\x32\x0f.Vault.UserAuth\x12?\n\x17\x62reachWatchSecurityData\x18\x19 \x03(\x0b\x32\x1e.Vault.BreachWatchSecurityData\x12/\n\x0freusedPasswords\x18\x1a \x01(\x0b\x32\x16.Vault.ReusedPasswords\x12\x1a\n\x12removedUserFolders\x18\x1b \x03(\x0c\x12\x1c\n\x14removedSharedFolders\x18\x1c \x03(\x0c\x12\x45\n\x1eremovedUserFolderSharedFolders\x18\x1d \x03(\x0b\x32\x1d.Vault.UserFolderSharedFolder\x12=\n\x1aremovedSharedFolderFolders\x18\x1e \x03(\x0b\x32\x19.Vault.SharedFolderFolder\x12\x16\n\x0eremovedRecords\x18\x1f \x03(\x0c\x12-\n\x12removedRecordLinks\x18 \x03(\x0b\x32\x11.Vault.RecordLink\x12\x39\n\x18removedUserFolderRecords\x18! \x03(\x0b\x32\x17.Vault.UserFolderRecord\x12=\n\x1aremovedSharedFolderRecords\x18\" \x03(\x0b\x32\x19.Vault.SharedFolderRecord\x12I\n removedSharedFolderFolderRecords\x18# \x03(\x0b\x32\x1f.Vault.SharedFolderFolderRecord\x12\x39\n\x18removedSharedFolderUsers\x18$ \x03(\x0b\x32\x17.Vault.SharedFolderUser\x12\x39\n\x18removedSharedFolderTeams\x18% \x03(\x0b\x32\x17.Vault.SharedFolderTeam\x12\x14\n\x0cremovedTeams\x18& \x03(\x0c\x12&\n\x0cksmAppShares\x18\' \x03(\x0b\x32\x10.Vault.KsmChange\x12\'\n\rksmAppClients\x18( \x03(\x0b\x32\x10.Vault.KsmChange\x12\x30\n\x10shareInvitations\x18) \x03(\x0b\x32\x16.Vault.ShareInvitation\x12+\n\x0b\x64iagnostics\x18* \x01(\x0b\x32\x16.Vault.SyncDiagnostics\x12.\n\x0frecordRotations\x18+ \x03(\x0b\x32\x15.Vault.RecordRotation\x12\x1a\n\x05users\x18, \x03(\x0b\x32\x0b.Vault.User\x12\x14\n\x0cremovedUsers\x18- \x03(\x0c\x12\x33\n\x11securityScoreData\x18. \x03(\x0b\x32\x18.Vault.SecurityScoreData\x12\x41\n\x10notificationSync\x18/ \x03(\x0b\x32\'.NotificationCenter.NotificationWrapper\"\x92\x01\n\nUserFolder\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x15\n\ruserFolderKey\x18\x03 \x01(\x0c\x12\'\n\x07keyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x10\n\x08revision\x18\x05 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\"\xd5\x02\n\x0cSharedFolder\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x17\n\x0fsharedFolderKey\x18\x03 \x01(\x0c\x12\'\n\x07keyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\x12\x1c\n\x14\x64\x65\x66\x61ultManageRecords\x18\x06 \x01(\x08\x12\x1a\n\x12\x64\x65\x66\x61ultManageUsers\x18\x07 \x01(\x08\x12\x16\n\x0e\x64\x65\x66\x61ultCanEdit\x18\x08 \x01(\x08\x12\x19\n\x11\x64\x65\x66\x61ultCanReshare\x18\t \x01(\x08\x12\'\n\x0b\x63\x61\x63heStatus\x18\n \x01(\x0e\x32\x12.Vault.CacheStatus\x12\r\n\x05owner\x18\x0b \x01(\t\x12\x17\n\x0fownerAccountUid\x18\x0c \x01(\x0c\x12\x0c\n\x04name\x18\r \x01(\x0c\"V\n\x16UserFolderSharedFolder\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"\xbb\x01\n\x12SharedFolderFolder\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\tfolderUid\x18\x02 \x01(\x0c\x12\x11\n\tparentUid\x18\x03 \x01(\x0c\x12\x1d\n\x15sharedFolderFolderKey\x18\x04 \x01(\x0c\x12\'\n\x07keyType\x18\x05 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x10\n\x08revision\x18\x06 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\"l\n\x0fSharedFolderKey\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderKey\x18\x02 \x01(\x0c\x12\'\n\x07keyType\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\xc3\x02\n\x04Team\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07teamKey\x18\x03 \x01(\x0c\x12+\n\x0bteamKeyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x16\n\x0eteamPrivateKey\x18\x05 \x01(\x0c\x12\x14\n\x0crestrictEdit\x18\x06 \x01(\x08\x12\x15\n\rrestrictShare\x18\x07 \x01(\x08\x12\x14\n\x0crestrictView\x18\x08 \x01(\x08\x12\x1c\n\x14removedSharedFolders\x18\t \x03(\x0c\x12\x30\n\x10sharedFolderKeys\x18\n \x03(\x0b\x32\x16.Vault.SharedFolderKey\x12\x19\n\x11teamEccPrivateKey\x18\x0b \x01(\x0c\x12\x18\n\x10teamEccPublicKey\x18\x0c \x01(\x0c\"\xbf\x01\n\x06Record\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0f\n\x07version\x18\x03 \x01(\x05\x12\x0e\n\x06shared\x18\x04 \x01(\x08\x12\x1a\n\x12\x63lientModifiedTime\x18\x05 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\r\n\x05\x65xtra\x18\x07 \x01(\x0c\x12\r\n\x05udata\x18\x08 \x01(\t\x12\x10\n\x08\x66ileSize\x18\t \x01(\x03\x12\x15\n\rthumbnailSize\x18\n \x01(\x03\"b\n\nRecordLink\x12\x17\n\x0fparentRecordUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63hildRecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x10\n\x08revision\x18\x04 \x01(\x03\"J\n\x10UserFolderRecord\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"k\n\x18SharedFolderFolderRecord\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\tfolderUid\x18\x02 \x01(\x0c\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08revision\x18\x04 \x01(\x03\"0\n\rNonSharedData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x9f\x02\n\x0eRecordMetaData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\r\n\x05owner\x18\x02 \x01(\x08\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12-\n\rrecordKeyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x10\n\x08\x63\x61nShare\x18\x05 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x06 \x01(\x08\x12\x17\n\x0fownerAccountUid\x18\x07 \x01(\x0c\x12\x12\n\nexpiration\x18\x08 \x01(\x03\x12\x42\n\x1a\x65xpirationNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x15\n\rownerUsername\x18\n \x01(\t\"2\n\rSharingChange\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06shared\x18\x02 \x01(\x08\">\n\x07Profile\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x13\n\x0bprofileName\x18\x02 \x01(\t\x12\x10\n\x08revision\x18\x03 \x01(\x03\"+\n\nProfilePic\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x10\n\x08revision\x18\x02 \x01(\x03\"p\n\x11PendingTeamMember\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x15\n\ruserPublicKey\x18\x02 \x01(\x0c\x12\x10\n\x08teamUids\x18\x03 \x03(\x0c\x12\x18\n\x10userEccPublicKey\x18\x04 \x01(\x0c\"\xa6\x01\n\x11\x42reachWatchRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12.\n\x04type\x18\x03 \x01(\x0e\x32 .BreachWatch.BreachWatchInfoType\x12\x11\n\tscannedBy\x18\x04 \x01(\t\x12\x10\n\x08revision\x18\x05 \x01(\x03\x12\x1b\n\x13scannedByAccountUid\x18\x06 \x01(\x0c\"\xb4\x01\n\x08UserAuth\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12,\n\tloginType\x18\x02 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x0f\n\x07\x64\x65leted\x18\x03 \x01(\x08\x12\x12\n\niterations\x18\x04 \x01(\x05\x12\x0c\n\x04salt\x18\x05 \x01(\x0c\x12\x1a\n\x12\x65ncryptedClientKey\x18\x06 \x01(\x0c\x12\x10\n\x08revision\x18\x07 \x01(\x03\x12\x0c\n\x04name\x18\x08 \x01(\t\">\n\x17\x42reachWatchSecurityData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\"2\n\x0fReusedPasswords\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\x12\x10\n\x08revision\x18\x02 \x01(\x03\"\xa9\x02\n\x12SharedFolderRecord\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x10\n\x08\x63\x61nShare\x18\x04 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x05 \x01(\x08\x12\x17\n\x0fownerAccountUid\x18\x06 \x01(\x0c\x12\x12\n\nexpiration\x18\x07 \x01(\x03\x12\r\n\x05owner\x18\x08 \x01(\x08\x12\x42\n\x1a\x65xpirationNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x15\n\rownerUsername\x18\n \x01(\t\x12\x1a\n\x12rotateOnExpiration\x18\x0b \x01(\x08\"\xf1\x01\n\x10SharedFolderUser\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x15\n\rmanageRecords\x18\x03 \x01(\x08\x12\x13\n\x0bmanageUsers\x18\x04 \x01(\x08\x12\x12\n\naccountUid\x18\x05 \x01(\x0c\x12\x12\n\nexpiration\x18\x06 \x01(\x03\x12\x42\n\x1a\x65xpirationNotificationType\x18\x07 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\xea\x01\n\x10SharedFolderTeam\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x0f\n\x07teamUid\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x15\n\rmanageRecords\x18\x04 \x01(\x08\x12\x13\n\x0bmanageUsers\x18\x05 \x01(\x08\x12\x12\n\nexpiration\x18\x06 \x01(\x03\x12\x42\n\x1a\x65xpirationNotificationType\x18\x07 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\x8a\x01\n\tKsmChange\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x64\x65tailId\x18\x02 \x01(\x0c\x12\x0f\n\x07removed\x18\x03 \x01(\x08\x12\x30\n\rappClientType\x18\x04 \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x12\n\nexpiration\x18\x05 \x01(\x03\"#\n\x0fShareInvitation\x12\x10\n\x08username\x18\x01 \x01(\t\",\n\x04User\x12\x12\n\naccountUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\"{\n\x0fSyncDiagnostics\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x0e\n\x06userId\x18\x02 \x01(\x05\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x10\n\x08syncedTo\x18\x04 \x01(\x03\x12\x11\n\tsyncingTo\x18\x05 \x01(\x03\"\xee\x01\n\x0eRecordRotation\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x03 \x01(\x0c\x12\x10\n\x08schedule\x18\x04 \x01(\t\x12\x15\n\rpwdComplexity\x18\x05 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x06 \x01(\x08\x12\x13\n\x0bresourceUid\x18\x07 \x01(\x0c\x12\x14\n\x0clastRotation\x18\x08 \x01(\x03\x12\x37\n\x12lastRotationStatus\x18\t \x01(\x0e\x32\x1b.Vault.RecordRotationStatus\"F\n\x11SecurityScoreData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"3\n\x1d\x42reachWatchGetSyncDataRequest\x12\x12\n\nrecordUids\x18\x01 \x03(\x0c\"\xb3\x01\n\x1e\x42reachWatchGetSyncDataResponse\x12\x34\n\x12\x62reachWatchRecords\x18\x01 \x03(\x0b\x32\x18.Vault.BreachWatchRecord\x12?\n\x17\x62reachWatchSecurityData\x18\x02 \x03(\x0b\x32\x1e.Vault.BreachWatchSecurityData\x12\x1a\n\x05users\x18\x03 \x03(\x0b\x32\x0b.Vault.User*\"\n\x0b\x43\x61\x63heStatus\x12\x08\n\x04KEEP\x10\x00\x12\t\n\x05\x43LEAR\x10\x01*f\n\x14RecordRotationStatus\x12\x14\n\x10RRST_NOT_ROTATED\x10\x00\x12\x14\n\x10RRST_IN_PROGRESS\x10\x01\x12\x10\n\x0cRRST_SUCCESS\x10\x02\x12\x10\n\x0cRRST_FAILURE\x10\x03\x42!\n\x18\x63om.keepersecurity.protoB\x05Vaultb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eSyncDown.proto\x12\x05Vault\x1a\x0crecord.proto\x1a\x11\x62reachwatch.proto\x1a\x10\x41PIRequest.proto\x1a\x10\x65nterprise.proto\x1a\x18NotificationCenter.proto\"A\n\x0fSyncDownRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x61taVersion\x18\x02 \x01(\x05\"\x81\x11\n\x10SyncDownResponse\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\x12\'\n\x0b\x63\x61\x63heStatus\x18\x03 \x01(\x0e\x32\x12.Vault.CacheStatus\x12&\n\x0buserFolders\x18\x04 \x03(\x0b\x32\x11.Vault.UserFolder\x12*\n\rsharedFolders\x18\x05 \x03(\x0b\x32\x13.Vault.SharedFolder\x12>\n\x17userFolderSharedFolders\x18\x06 \x03(\x0b\x32\x1d.Vault.UserFolderSharedFolder\x12\x36\n\x13sharedFolderFolders\x18\x07 \x03(\x0b\x32\x19.Vault.SharedFolderFolder\x12\x1e\n\x07records\x18\x08 \x03(\x0b\x32\r.Vault.Record\x12-\n\x0erecordMetaData\x18\t \x03(\x0b\x32\x15.Vault.RecordMetaData\x12+\n\rnonSharedData\x18\n \x03(\x0b\x32\x14.Vault.NonSharedData\x12&\n\x0brecordLinks\x18\x0b \x03(\x0b\x32\x11.Vault.RecordLink\x12\x32\n\x11userFolderRecords\x18\x0c \x03(\x0b\x32\x17.Vault.UserFolderRecord\x12\x36\n\x13sharedFolderRecords\x18\r \x03(\x0b\x32\x19.Vault.SharedFolderRecord\x12\x42\n\x19sharedFolderFolderRecords\x18\x0e \x03(\x0b\x32\x1f.Vault.SharedFolderFolderRecord\x12\x32\n\x11sharedFolderUsers\x18\x0f \x03(\x0b\x32\x17.Vault.SharedFolderUser\x12\x32\n\x11sharedFolderTeams\x18\x10 \x03(\x0b\x32\x17.Vault.SharedFolderTeam\x12\x1a\n\x12recordAddAuditData\x18\x11 \x03(\x0c\x12\x1a\n\x05teams\x18\x12 \x03(\x0b\x32\x0b.Vault.Team\x12,\n\x0esharingChanges\x18\x13 \x03(\x0b\x32\x14.Vault.SharingChange\x12\x1f\n\x07profile\x18\x14 \x01(\x0b\x32\x0e.Vault.Profile\x12%\n\nprofilePic\x18\x15 \x01(\x0b\x32\x11.Vault.ProfilePic\x12\x34\n\x12pendingTeamMembers\x18\x16 \x03(\x0b\x32\x18.Vault.PendingTeamMember\x12\x34\n\x12\x62reachWatchRecords\x18\x17 \x03(\x0b\x32\x18.Vault.BreachWatchRecord\x12\"\n\tuserAuths\x18\x18 \x03(\x0b\x32\x0f.Vault.UserAuth\x12?\n\x17\x62reachWatchSecurityData\x18\x19 \x03(\x0b\x32\x1e.Vault.BreachWatchSecurityData\x12/\n\x0freusedPasswords\x18\x1a \x01(\x0b\x32\x16.Vault.ReusedPasswords\x12\x1a\n\x12removedUserFolders\x18\x1b \x03(\x0c\x12\x1c\n\x14removedSharedFolders\x18\x1c \x03(\x0c\x12\x45\n\x1eremovedUserFolderSharedFolders\x18\x1d \x03(\x0b\x32\x1d.Vault.UserFolderSharedFolder\x12=\n\x1aremovedSharedFolderFolders\x18\x1e \x03(\x0b\x32\x19.Vault.SharedFolderFolder\x12\x16\n\x0eremovedRecords\x18\x1f \x03(\x0c\x12-\n\x12removedRecordLinks\x18 \x03(\x0b\x32\x11.Vault.RecordLink\x12\x39\n\x18removedUserFolderRecords\x18! \x03(\x0b\x32\x17.Vault.UserFolderRecord\x12=\n\x1aremovedSharedFolderRecords\x18\" \x03(\x0b\x32\x19.Vault.SharedFolderRecord\x12I\n removedSharedFolderFolderRecords\x18# \x03(\x0b\x32\x1f.Vault.SharedFolderFolderRecord\x12\x39\n\x18removedSharedFolderUsers\x18$ \x03(\x0b\x32\x17.Vault.SharedFolderUser\x12\x39\n\x18removedSharedFolderTeams\x18% \x03(\x0b\x32\x17.Vault.SharedFolderTeam\x12\x14\n\x0cremovedTeams\x18& \x03(\x0c\x12&\n\x0cksmAppShares\x18\' \x03(\x0b\x32\x10.Vault.KsmChange\x12\'\n\rksmAppClients\x18( \x03(\x0b\x32\x10.Vault.KsmChange\x12\x30\n\x10shareInvitations\x18) \x03(\x0b\x32\x16.Vault.ShareInvitation\x12+\n\x0b\x64iagnostics\x18* \x01(\x0b\x32\x16.Vault.SyncDiagnostics\x12.\n\x0frecordRotations\x18+ \x03(\x0b\x32\x15.Vault.RecordRotation\x12\x1a\n\x05users\x18, \x03(\x0b\x32\x0b.Vault.User\x12\x14\n\x0cremovedUsers\x18- \x03(\x0c\x12\x33\n\x11securityScoreData\x18. \x03(\x0b\x32\x18.Vault.SecurityScoreData\x12\x41\n\x10notificationSync\x18/ \x03(\x0b\x32\'.NotificationCenter.NotificationWrapper\"\x92\x01\n\nUserFolder\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x15\n\ruserFolderKey\x18\x03 \x01(\x0c\x12\'\n\x07keyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x10\n\x08revision\x18\x05 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\"\xd5\x02\n\x0cSharedFolder\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x17\n\x0fsharedFolderKey\x18\x03 \x01(\x0c\x12\'\n\x07keyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\x12\x1c\n\x14\x64\x65\x66\x61ultManageRecords\x18\x06 \x01(\x08\x12\x1a\n\x12\x64\x65\x66\x61ultManageUsers\x18\x07 \x01(\x08\x12\x16\n\x0e\x64\x65\x66\x61ultCanEdit\x18\x08 \x01(\x08\x12\x19\n\x11\x64\x65\x66\x61ultCanReshare\x18\t \x01(\x08\x12\'\n\x0b\x63\x61\x63heStatus\x18\n \x01(\x0e\x32\x12.Vault.CacheStatus\x12\r\n\x05owner\x18\x0b \x01(\t\x12\x17\n\x0fownerAccountUid\x18\x0c \x01(\x0c\x12\x0c\n\x04name\x18\r \x01(\x0c\"V\n\x16UserFolderSharedFolder\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"\xbb\x01\n\x12SharedFolderFolder\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\tfolderUid\x18\x02 \x01(\x0c\x12\x11\n\tparentUid\x18\x03 \x01(\x0c\x12\x1d\n\x15sharedFolderFolderKey\x18\x04 \x01(\x0c\x12\'\n\x07keyType\x18\x05 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x10\n\x08revision\x18\x06 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\"l\n\x0fSharedFolderKey\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderKey\x18\x02 \x01(\x0c\x12\'\n\x07keyType\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\xc3\x02\n\x04Team\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07teamKey\x18\x03 \x01(\x0c\x12+\n\x0bteamKeyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x16\n\x0eteamPrivateKey\x18\x05 \x01(\x0c\x12\x14\n\x0crestrictEdit\x18\x06 \x01(\x08\x12\x15\n\rrestrictShare\x18\x07 \x01(\x08\x12\x14\n\x0crestrictView\x18\x08 \x01(\x08\x12\x1c\n\x14removedSharedFolders\x18\t \x03(\x0c\x12\x30\n\x10sharedFolderKeys\x18\n \x03(\x0b\x32\x16.Vault.SharedFolderKey\x12\x19\n\x11teamEccPrivateKey\x18\x0b \x01(\x0c\x12\x18\n\x10teamEccPublicKey\x18\x0c \x01(\x0c\"\xbf\x01\n\x06Record\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0f\n\x07version\x18\x03 \x01(\x05\x12\x0e\n\x06shared\x18\x04 \x01(\x08\x12\x1a\n\x12\x63lientModifiedTime\x18\x05 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\r\n\x05\x65xtra\x18\x07 \x01(\x0c\x12\r\n\x05udata\x18\x08 \x01(\t\x12\x10\n\x08\x66ileSize\x18\t \x01(\x03\x12\x15\n\rthumbnailSize\x18\n \x01(\x03\"b\n\nRecordLink\x12\x17\n\x0fparentRecordUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63hildRecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x10\n\x08revision\x18\x04 \x01(\x03\"J\n\x10UserFolderRecord\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"k\n\x18SharedFolderFolderRecord\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\tfolderUid\x18\x02 \x01(\x0c\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08revision\x18\x04 \x01(\x03\"0\n\rNonSharedData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x9f\x02\n\x0eRecordMetaData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\r\n\x05owner\x18\x02 \x01(\x08\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12-\n\rrecordKeyType\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x10\n\x08\x63\x61nShare\x18\x05 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x06 \x01(\x08\x12\x17\n\x0fownerAccountUid\x18\x07 \x01(\x0c\x12\x12\n\nexpiration\x18\x08 \x01(\x03\x12\x42\n\x1a\x65xpirationNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x15\n\rownerUsername\x18\n \x01(\t\"2\n\rSharingChange\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06shared\x18\x02 \x01(\x08\">\n\x07Profile\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x13\n\x0bprofileName\x18\x02 \x01(\t\x12\x10\n\x08revision\x18\x03 \x01(\x03\"+\n\nProfilePic\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x10\n\x08revision\x18\x02 \x01(\x03\"p\n\x11PendingTeamMember\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x15\n\ruserPublicKey\x18\x02 \x01(\x0c\x12\x10\n\x08teamUids\x18\x03 \x03(\x0c\x12\x18\n\x10userEccPublicKey\x18\x04 \x01(\x0c\"\xa6\x01\n\x11\x42reachWatchRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12.\n\x04type\x18\x03 \x01(\x0e\x32 .BreachWatch.BreachWatchInfoType\x12\x11\n\tscannedBy\x18\x04 \x01(\t\x12\x10\n\x08revision\x18\x05 \x01(\x03\x12\x1b\n\x13scannedByAccountUid\x18\x06 \x01(\x0c\"\xb4\x01\n\x08UserAuth\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12,\n\tloginType\x18\x02 \x01(\x0e\x32\x19.Authentication.LoginType\x12\x0f\n\x07\x64\x65leted\x18\x03 \x01(\x08\x12\x12\n\niterations\x18\x04 \x01(\x05\x12\x0c\n\x04salt\x18\x05 \x01(\x0c\x12\x1a\n\x12\x65ncryptedClientKey\x18\x06 \x01(\x0c\x12\x10\n\x08revision\x18\x07 \x01(\x03\x12\x0c\n\x04name\x18\x08 \x01(\t\">\n\x17\x42reachWatchSecurityData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\"2\n\x0fReusedPasswords\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\x12\x10\n\x08revision\x18\x02 \x01(\x03\"\xa9\x02\n\x12SharedFolderRecord\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x10\n\x08\x63\x61nShare\x18\x04 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x05 \x01(\x08\x12\x17\n\x0fownerAccountUid\x18\x06 \x01(\x0c\x12\x12\n\nexpiration\x18\x07 \x01(\x03\x12\r\n\x05owner\x18\x08 \x01(\x08\x12\x42\n\x1a\x65xpirationNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x15\n\rownerUsername\x18\n \x01(\t\x12\x1a\n\x12rotateOnExpiration\x18\x0b \x01(\x08\"\xf1\x01\n\x10SharedFolderUser\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x15\n\rmanageRecords\x18\x03 \x01(\x08\x12\x13\n\x0bmanageUsers\x18\x04 \x01(\x08\x12\x12\n\naccountUid\x18\x05 \x01(\x0c\x12\x12\n\nexpiration\x18\x06 \x01(\x03\x12\x42\n\x1a\x65xpirationNotificationType\x18\x07 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\xea\x01\n\x10SharedFolderTeam\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x0f\n\x07teamUid\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x15\n\rmanageRecords\x18\x04 \x01(\x08\x12\x13\n\x0bmanageUsers\x18\x05 \x01(\x08\x12\x12\n\nexpiration\x18\x06 \x01(\x03\x12\x42\n\x1a\x65xpirationNotificationType\x18\x07 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\x8a\x01\n\tKsmChange\x12\x14\n\x0c\x61ppRecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x64\x65tailId\x18\x02 \x01(\x0c\x12\x0f\n\x07removed\x18\x03 \x01(\x08\x12\x30\n\rappClientType\x18\x04 \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x12\n\nexpiration\x18\x05 \x01(\x03\"#\n\x0fShareInvitation\x12\x10\n\x08username\x18\x01 \x01(\t\",\n\x04User\x12\x12\n\naccountUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\"{\n\x0fSyncDiagnostics\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x0e\n\x06userId\x18\x02 \x01(\x05\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x10\n\x08syncedTo\x18\x04 \x01(\x03\x12\x11\n\tsyncingTo\x18\x05 \x01(\x03\"\xee\x01\n\x0eRecordRotation\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x03 \x01(\x0c\x12\x10\n\x08schedule\x18\x04 \x01(\t\x12\x15\n\rpwdComplexity\x18\x05 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x06 \x01(\x08\x12\x13\n\x0bresourceUid\x18\x07 \x01(\x0c\x12\x14\n\x0clastRotation\x18\x08 \x01(\x03\x12\x37\n\x12lastRotationStatus\x18\t \x01(\x0e\x32\x1b.Vault.RecordRotationStatus\"F\n\x11SecurityScoreData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\"3\n\x1d\x42reachWatchGetSyncDataRequest\x12\x12\n\nrecordUids\x18\x01 \x03(\x0c\"\xb3\x01\n\x1e\x42reachWatchGetSyncDataResponse\x12\x34\n\x12\x62reachWatchRecords\x18\x01 \x03(\x0b\x32\x18.Vault.BreachWatchRecord\x12?\n\x17\x62reachWatchSecurityData\x18\x02 \x03(\x0b\x32\x1e.Vault.BreachWatchSecurityData\x12\x1a\n\x05users\x18\x03 \x03(\x0b\x32\x0b.Vault.User\"6\n\x18GetAccountUidMapResponse\x12\x1a\n\x05users\x18\x01 \x03(\x0b\x32\x0b.Vault.User*\"\n\x0b\x43\x61\x63heStatus\x12\x08\n\x04KEEP\x10\x00\x12\t\n\x05\x43LEAR\x10\x01*f\n\x14RecordRotationStatus\x12\x14\n\x10RRST_NOT_ROTATED\x10\x00\x12\x14\n\x10RRST_IN_PROGRESS\x10\x01\x12\x10\n\x0cRRST_SUCCESS\x10\x02\x12\x10\n\x0cRRST_FAILURE\x10\x03\x42!\n\x18\x63om.keepersecurity.protoB\x05Vaultb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,10 +29,10 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\005Vault' - _globals['_CACHESTATUS']._serialized_start=6814 - _globals['_CACHESTATUS']._serialized_end=6848 - _globals['_RECORDROTATIONSTATUS']._serialized_start=6850 - _globals['_RECORDROTATIONSTATUS']._serialized_end=6952 + _globals['_CACHESTATUS']._serialized_start=6870 + _globals['_CACHESTATUS']._serialized_end=6904 + _globals['_RECORDROTATIONSTATUS']._serialized_start=6906 + _globals['_RECORDROTATIONSTATUS']._serialized_end=7008 _globals['_SYNCDOWNREQUEST']._serialized_start=120 _globals['_SYNCDOWNREQUEST']._serialized_end=185 _globals['_SYNCDOWNRESPONSE']._serialized_start=188 @@ -107,4 +99,6 @@ _globals['_BREACHWATCHGETSYNCDATAREQUEST']._serialized_end=6630 _globals['_BREACHWATCHGETSYNCDATARESPONSE']._serialized_start=6633 _globals['_BREACHWATCHGETSYNCDATARESPONSE']._serialized_end=6812 + _globals['_GETACCOUNTUIDMAPRESPONSE']._serialized_start=6814 + _globals['_GETACCOUNTUIDMAPRESPONSE']._serialized_end=6868 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi index a1e9faec..e4a7e178 100644 --- a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi @@ -582,3 +582,9 @@ class BreachWatchGetSyncDataResponse(_message.Message): breachWatchSecurityData: _containers.RepeatedCompositeFieldContainer[BreachWatchSecurityData] users: _containers.RepeatedCompositeFieldContainer[User] def __init__(self, breachWatchRecords: _Optional[_Iterable[_Union[BreachWatchRecord, _Mapping]]] = ..., breachWatchSecurityData: _Optional[_Iterable[_Union[BreachWatchSecurityData, _Mapping]]] = ..., users: _Optional[_Iterable[_Union[User, _Mapping]]] = ...) -> None: ... + +class GetAccountUidMapResponse(_message.Message): + __slots__ = ("users",) + USERS_FIELD_NUMBER: _ClassVar[int] + users: _containers.RepeatedCompositeFieldContainer[User] + def __init__(self, users: _Optional[_Iterable[_Union[User, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/automator_pb2.py b/keepersdk-package/src/keepersdk/proto/automator_pb2.py index c404e412..8258c6e9 100644 --- a/keepersdk-package/src/keepersdk/proto/automator_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/automator_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: automator.proto -# Protobuf Python Version: 5.28.3 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 3, - '', - 'automator.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -24,6 +16,7 @@ from . import ssocloud_pb2 as ssocloud__pb2 from . import enterprise_pb2 as enterprise__pb2 +from . import version_pb2 as version__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61utomator.proto\x12\tAutomator\x1a\x0essocloud.proto\x1a\x10\x65nterprise.proto\x1a\rversion.proto\"\xbf\x02\n\x15\x41utomatorSettingValue\x12\x11\n\tsettingId\x18\x01 \x01(\x03\x12\x15\n\rsettingTypeId\x18\x02 \x01(\x05\x12\x12\n\nsettingTag\x18\x03 \x01(\t\x12\x13\n\x0bsettingName\x18\x04 \x01(\t\x12\x14\n\x0csettingValue\x18\x05 \x01(\t\x12$\n\x08\x64\x61taType\x18\x06 \x01(\x0e\x32\x12.SsoCloud.DataType\x12\x14\n\x0clastModified\x18\x07 \x01(\t\x12\x10\n\x08\x66romFile\x18\x08 \x01(\x08\x12\x11\n\tencrypted\x18\t \x01(\x08\x12\x0f\n\x07\x65ncoded\x18\n \x01(\x08\x12\x10\n\x08\x65\x64itable\x18\x0b \x01(\x08\x12\x12\n\ntranslated\x18\x0c \x01(\x08\x12\x13\n\x0buserVisible\x18\r \x01(\x08\x12\x10\n\x08required\x18\x0e \x01(\x08\"\xee\x02\n\x14\x41pproveDeviceRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12O\n\x1dssoAuthenticationProtocolType\x18\x02 \x01(\x0e\x32(.Automator.SsoAuthenticationProtocolType\x12\x13\n\x0b\x61uthMessage\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x05 \x01(\x0c\x12\x1c\n\x14serverEccPublicKeyId\x18\x06 \x01(\x05\x12\x1c\n\x14userEncryptedDataKey\x18\x07 \x01(\x0c\x12>\n\x18userEncryptedDataKeyType\x18\x08 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x11\n\tipAddress\x18\t \x01(\t\x12\x11\n\tisTesting\x18\n \x01(\x08\x12\x11\n\tisEccOnly\x18\x0b \x01(\x08\"\xa9\x02\n\x0cSetupRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x1c\n\x14serverEccPublicKeyId\x18\x02 \x01(\x05\x12\x31\n\x0e\x61utomatorState\x18\x03 \x01(\x0e\x32\x19.Automator.AutomatorState\x12(\n encryptedEnterprisePrivateEccKey\x18\x04 \x01(\x0c\x12(\n encryptedEnterprisePrivateRsaKey\x18\x05 \x01(\x0c\x12\x32\n\x0f\x61utomatorSkills\x18\x06 \x03(\x0b\x32\x19.Automator.AutomatorSkill\x12\x18\n\x10\x65ncryptedTreeKey\x18\x07 \x01(\x0c\x12\x11\n\tisEccOnly\x18\x08 \x01(\x08\"U\n\rStatusRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x1c\n\x14serverEccPublicKeyId\x18\x02 \x01(\x05\x12\x11\n\tisEccOnly\x18\x03 \x01(\x08\"\xa3\x04\n\x11InitializeRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x13\n\x0bidpMetadata\x18\x02 \x01(\t\x12\x1d\n\x15idpSigningCertificate\x18\x03 \x01(\x0c\x12\x13\n\x0bssoEntityId\x18\x04 \x01(\t\x12\x14\n\x0c\x65mailMapping\x18\x05 \x01(\t\x12\x18\n\x10\x66irstnameMapping\x18\x06 \x01(\t\x12\x17\n\x0flastnameMapping\x18\x07 \x01(\t\x12\x10\n\x08\x64isabled\x18\x08 \x01(\x08\x12\x1c\n\x14serverEccPublicKeyId\x18\t \x01(\x05\x12\x0e\n\x06\x63onfig\x18\n \x01(\x0c\x12\x0f\n\x07sslMode\x18\x0b \x01(\t\x12\x14\n\x0cpersistState\x18\x0c \x01(\x08\x12\x17\n\x0f\x64isableSniCheck\x18\r \x01(\x08\x12\x1e\n\x16sslCertificateFilename\x18\x0e \x01(\t\x12\"\n\x1asslCertificateFilePassword\x18\x0f \x01(\t\x12!\n\x19sslCertificateKeyPassword\x18\x10 \x01(\t\x12\x1e\n\x16sslCertificateContents\x18\x11 \x01(\x0c\x12\x15\n\rautomatorHost\x18\x12 \x01(\t\x12\x15\n\rautomatorPort\x18\x13 \x01(\t\x12\x0f\n\x07ipAllow\x18\x14 \x01(\t\x12\x0e\n\x06ipDeny\x18\x15 \x01(\t\x12\x11\n\tisEccOnly\x18\x16 \x01(\x08\"\xa6\x02\n\x16NotInitializedResponse\x12 \n\x18\x61utomatorTransmissionKey\x18\x01 \x01(\x0c\x12\x1a\n\x12signingCertificate\x18\x02 \x01(\x0c\x12\"\n\x1asigningCertificateFilename\x18\x03 \x01(\t\x12\"\n\x1asigningCertificatePassword\x18\x04 \x01(\t\x12\x1a\n\x12signingKeyPassword\x18\x05 \x01(\t\x12>\n\x18signingCertificateFormat\x18\x06 \x01(\x0e\x32\x1c.Automator.CertificateFormat\x12\x1a\n\x12\x61utomatorPublicKey\x18\x07 \x01(\x0c\x12\x0e\n\x06\x63onfig\x18\x08 \x01(\x0c\"\xa5\x04\n\x11\x41utomatorResponse\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x0f\n\x07\x65nabled\x18\x02 \x01(\x08\x12\x11\n\ttimestamp\x18\x03 \x01(\x03\x12\x39\n\rapproveDevice\x18\x04 \x01(\x0b\x32 .Automator.ApproveDeviceResponseH\x00\x12+\n\x06status\x18\x05 \x01(\x0b\x32\x19.Automator.StatusResponseH\x00\x12;\n\x0enotInitialized\x18\x06 \x01(\x0b\x32!.Automator.NotInitializedResponseH\x00\x12)\n\x05\x65rror\x18\x07 \x01(\x0b\x32\x18.Automator.ErrorResponseH\x00\x12\x45\n\x13\x61pproveTeamsForUser\x18\n \x01(\x0b\x32&.Automator.ApproveTeamsForUserResponseH\x00\x12\x37\n\x0c\x61pproveTeams\x18\x0b \x01(\x0b\x32\x1f.Automator.ApproveTeamsResponseH\x00\x12\x31\n\x0e\x61utomatorState\x18\x08 \x01(\x0e\x32\x19.Automator.AutomatorState\x12\x1d\n\x15\x61utomatorPublicEccKey\x18\t \x01(\x0c\x12)\n\x07version\x18\x0c \x01(\x0b\x32\x18.SemanticVersion.VersionB\n\n\x08response\"\x98\x01\n\x15\x41pproveDeviceResponse\x12\x10\n\x08\x61pproved\x18\x01 \x01(\x08\x12\x1c\n\x14\x65ncryptedUserDataKey\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\t\x12>\n\x18\x65ncryptedUserDataKeyType\x18\x04 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"\xd0\x03\n\x0eStatusResponse\x12\x13\n\x0binitialized\x18\x01 \x01(\x08\x12\x18\n\x10\x65nabledTimestamp\x18\x02 \x01(\x03\x12\x1c\n\x14initializedTimestamp\x18\x03 \x01(\x03\x12\x18\n\x10updatedTimestamp\x18\x04 \x01(\x03\x12\x1f\n\x17numberOfDevicesApproved\x18\x05 \x01(\x03\x12\x1d\n\x15numberOfDevicesDenied\x18\x06 \x01(\x03\x12\x16\n\x0enumberOfErrors\x18\x07 \x01(\x03\x12 \n\x18sslCertificateExpiration\x18\x08 \x01(\x03\x12\x41\n\x16notInitializedResponse\x18\t \x01(\x0b\x32!.Automator.NotInitializedResponse\x12\x0e\n\x06\x63onfig\x18\n \x01(\x0c\x12\'\n\x1fnumberOfTeamMembershipsApproved\x18\x0b \x01(\x03\x12%\n\x1dnumberOfTeamMembershipsDenied\x18\x0c \x01(\x03\x12\x1d\n\x15numberOfTeamsApproved\x18\r \x01(\x03\x12\x1b\n\x13numberOfTeamsDenied\x18\x0e \x01(\x03\" \n\rErrorResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\"X\n\x08LogEntry\x12\x12\n\nserverTime\x18\x01 \x01(\t\x12\x14\n\x0cmessageLevel\x18\x02 \x01(\t\x12\x11\n\tcomponent\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\"b\n\rAdminResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12/\n\rautomatorInfo\x18\x03 \x03(\x0b\x32\x18.Automator.AutomatorInfo\"\xee\x02\n\rAutomatorInfo\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x32\n\x0f\x61utomatorSkills\x18\x06 \x03(\x0b\x32\x19.Automator.AutomatorSkill\x12@\n\x16\x61utomatorSettingValues\x18\x07 \x03(\x0b\x32 .Automator.AutomatorSettingValue\x12)\n\x06status\x18\x08 \x01(\x0b\x32\x19.Automator.StatusResponse\x12\'\n\nlogEntries\x18\t \x03(\x0b\x32\x13.Automator.LogEntry\x12\x31\n\x0e\x61utomatorState\x18\n \x01(\x0e\x32\x19.Automator.AutomatorState\x12\x0f\n\x07version\x18\x0b \x01(\t\"e\n\x1b\x41\x64minCreateAutomatorRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x05skill\x18\x03 \x01(\x0b\x32\x19.Automator.AutomatorSkill\"2\n\x1b\x41\x64minDeleteAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"1\n\x1f\x41\x64minGetAutomatorsOnNodeRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\">\n&AdminGetAutomatorsForEnterpriseRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\"/\n\x18\x41\x64minGetAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"C\n\x1b\x41\x64minEnableAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x0f\n\x07\x65nabled\x18\x02 \x01(\x08\"\xc8\x01\n\x19\x41\x64minEditAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x12\x0b\n\x03url\x18\x04 \x01(\t\x12(\n\nskillTypes\x18\x05 \x03(\x0e\x32\x14.Automator.SkillType\x12@\n\x16\x61utomatorSettingValues\x18\x06 \x03(\x0b\x32 .Automator.AutomatorSettingValue\"\xfc\x01\n\x1a\x41\x64minSetupAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x31\n\x0e\x61utomatorState\x18\x02 \x01(\x0e\x32\x19.Automator.AutomatorState\x12(\n encryptedEccEnterprisePrivateKey\x18\x03 \x01(\x0c\x12(\n encryptedRsaEnterprisePrivateKey\x18\x04 \x01(\x0c\x12(\n\nskillTypes\x18\x05 \x03(\x0e\x32\x14.Automator.SkillType\x12\x18\n\x10\x65ncryptedTreeKey\x18\x06 \x01(\x0c\"\xa6\x01\n\x1b\x41\x64minSetupAutomatorResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x13\n\x0b\x61utomatorId\x18\x03 \x01(\x03\x12\x31\n\x0e\x61utomatorState\x18\x04 \x01(\x0e\x32\x19.Automator.AutomatorState\x12\x1d\n\x15\x61utomatorEccPublicKey\x18\x05 \x01(\x0c\"2\n\x1b\x41\x64minAutomatorSkillsRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"_\n\x0e\x41utomatorSkill\x12\'\n\tskillType\x18\x01 \x01(\x0e\x32\x14.Automator.SkillType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x16\n\x0etranslatedName\x18\x03 \x01(\t\"t\n\x1c\x41\x64minAutomatorSkillsResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x32\n\x0f\x61utomatorSkills\x18\x03 \x03(\x0b\x32\x19.Automator.AutomatorSkill\"1\n\x1a\x41\x64minResetAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"6\n\x1f\x41\x64minInitializeAutomatorRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"/\n\x18\x41\x64minAutomatorLogRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"4\n\x1d\x41\x64minAutomatorLogClearRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\"\xe3\x02\n\x1a\x41pproveTeamsForUserRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12O\n\x1dssoAuthenticationProtocolType\x18\x02 \x01(\x0e\x32(.Automator.SsoAuthenticationProtocolType\x12\x13\n\x0b\x61uthMessage\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x1c\n\x14serverEccPublicKeyId\x18\x05 \x01(\x05\x12\x11\n\tipAddress\x18\x06 \x01(\t\x12\x15\n\ruserPublicKey\x18\x07 \x01(\x0c\x12\x33\n\x0fteamDescription\x18\x08 \x03(\x0b\x32\x1a.Automator.TeamDescription\x12\x11\n\tisTesting\x18\t \x01(\x08\x12\x11\n\tisEccOnly\x18\n \x01(\x08\x12\x18\n\x10userPublicKeyEcc\x18\x0b \x01(\x0c\"\x8a\x01\n\x0fTeamDescription\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x10\n\x08teamName\x18\x02 \x01(\t\x12\x18\n\x10\x65ncryptedTeamKey\x18\x03 \x01(\x0c\x12:\n\x14\x65ncryptedTeamKeyType\x18\x04 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"\x99\x01\n\x1b\x41pproveTeamsForUserResponse\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x45\n\x13\x61pproveTeamResponse\x18\x04 \x03(\x0b\x32(.Automator.ApproveOneTeamForUserResponse\"\xab\x02\n\x1d\x41pproveOneTeamForUserResponse\x12\x10\n\x08\x61pproved\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0f\n\x07teamUid\x18\x03 \x01(\x0c\x12\x10\n\x08teamName\x18\x04 \x01(\t\x12\x1c\n\x14userEncryptedTeamKey\x18\x05 \x01(\x0c\x12>\n\x18userEncryptedTeamKeyType\x18\x06 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12!\n\x19userEncryptedTeamKeyByEcc\x18\x07 \x01(\x0c\x12\x43\n\x1duserEncryptedTeamKeyByEccType\x18\x08 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"\xab\x02\n\x13\x41pproveTeamsRequest\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12O\n\x1dssoAuthenticationProtocolType\x18\x02 \x01(\x0e\x32(.Automator.SsoAuthenticationProtocolType\x12\x13\n\x0b\x61uthMessage\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x1c\n\x14serverEccPublicKeyId\x18\x05 \x01(\x05\x12\x11\n\tipAddress\x18\x06 \x01(\t\x12\x33\n\x0fteamDescription\x18\x07 \x03(\x0b\x32\x1a.Automator.TeamDescription\x12\x11\n\tisEccOnly\x18\x08 \x01(\x08\x12\x11\n\tisTesting\x18\t \x01(\x08\"|\n\x14\x41pproveTeamsResponse\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x03\x12\x0f\n\x07message\x18\x02 \x01(\t\x12>\n\x13\x61pproveTeamResponse\x18\x03 \x03(\x0b\x32!.Automator.ApproveOneTeamResponse\"\x9e\x04\n\x16\x41pproveOneTeamResponse\x12\x10\n\x08\x61pproved\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0f\n\x07teamUid\x18\x03 \x01(\x0c\x12\x10\n\x08teamName\x18\x04 \x01(\t\x12\x1b\n\x13\x65ncryptedTeamKeyCbc\x18\x05 \x01(\x0c\x12=\n\x17\x65ncryptedTeamKeyCbcType\x18\x06 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x1b\n\x13\x65ncryptedTeamKeyGcm\x18\x07 \x01(\x0c\x12=\n\x17\x65ncryptedTeamKeyGcmType\x18\x08 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x18\n\x10teamPublicKeyRsa\x18\t \x01(\x0c\x12\"\n\x1a\x65ncryptedTeamPrivateKeyRsa\x18\n \x01(\x0c\x12\x44\n\x1e\x65ncryptedTeamPrivateKeyRsaType\x18\x0b \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x18\n\x10teamPublicKeyEcc\x18\x0c \x01(\x0c\x12\"\n\x1a\x65ncryptedTeamPrivateKeyEcc\x18\r \x01(\x0c\x12\x44\n\x1e\x65ncryptedTeamPrivateKeyEccType\x18\x0e \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType*@\n\x1dSsoAuthenticationProtocolType\x12\x14\n\x10UNKNOWN_PROTOCOL\x10\x00\x12\t\n\x05SAML2\x10\x01*<\n\x11\x43\x65rtificateFormat\x12\x12\n\x0eUNKNOWN_FORMAT\x10\x00\x12\n\n\x06PKCS12\x10\x01\x12\x07\n\x03JKS\x10\x02*g\n\tSkillType\x12\x16\n\x12UNKNOWN_SKILL_TYPE\x10\x00\x12\x13\n\x0f\x44\x45VICE_APPROVAL\x10\x01\x12\x11\n\rTEAM_APPROVAL\x10\x02\x12\x1a\n\x16TEAM_FOR_USER_APPROVAL\x10\x03*\x87\x01\n\x0e\x41utomatorState\x12\x11\n\rUNKNOWN_STATE\x10\x00\x12\x0b\n\x07RUNNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x12\x18\n\x14NEEDS_INITIALIZATION\x10\x03\x12\x17\n\x13NEEDS_CRYPTO_STEP_1\x10\x04\x12\x17\n\x13NEEDS_CRYPTO_STEP_2\x10\x05\x42%\n\x18\x63om.keepersecurity.protoB\tAutomatorb\x06proto3') diff --git a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py index db903f40..33d35660 100644 --- a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: breachwatch.proto -# Protobuf Python Version: 5.28.3 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 3, - '', - 'breachwatch.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/client_pb2.py b/keepersdk-package/src/keepersdk/proto/client_pb2.py index 9c606a49..75860a15 100644 --- a/keepersdk-package/src/keepersdk/proto/client_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/client_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: client.proto -# Protobuf Python Version: 5.28.3 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 3, - '', - 'client.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py index 4d85cee7..bea2fedc 100644 --- a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: enterprise.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'enterprise.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -24,7 +16,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x65nterprise.proto\x12\nEnterprise\"\x84\x01\n\x18\x45nterpriseKeyPairRequest\x12\x1b\n\x13\x65nterprisePublicKey\x18\x01 \x01(\x0c\x12%\n\x1d\x65ncryptedEnterprisePrivateKey\x18\x02 \x01(\x0c\x12$\n\x07keyType\x18\x03 \x01(\x0e\x32\x13.Enterprise.KeyType\"\'\n\x14GetTeamMemberRequest\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\"}\n\x0e\x45nterpriseUser\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x1a\n\x12\x65nterpriseUsername\x18\x03 \x01(\t\x12\x14\n\x0cisShareAdmin\x18\x04 \x01(\x08\x12\x10\n\x08username\x18\x05 \x01(\t\"K\n\x15GetTeamMemberResponse\x12\x32\n\x0e\x65nterpriseUser\x18\x01 \x03(\x0b\x32\x1a.Enterprise.EnterpriseUser\"-\n\x11\x45nterpriseUserIds\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x03(\x03\"B\n\x19\x45nterprisePersonalAccount\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x16\n\x0eOBSOLETE_FIELD\x18\x02 \x01(\x0c\"S\n\x17\x45ncryptedTeamKeyRequest\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTeamKey\x18\x02 \x01(\x0c\x12\r\n\x05\x66orce\x18\x03 \x01(\x08\"+\n\x0fReEncryptedData\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"?\n\x12ReEncryptedRoleKey\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x18\n\x10\x65ncryptedRoleKey\x18\x02 \x01(\x0c\"P\n\x16ReEncryptedUserDataKey\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14userEncryptedDataKey\x18\x02 \x01(\x0c\"\xd8\x02\n\x1bNodeToManagedCompanyRequest\x12\x11\n\tcompanyId\x18\x01 \x01(\x05\x12*\n\x05nodes\x18\x02 \x03(\x0b\x32\x1b.Enterprise.ReEncryptedData\x12*\n\x05roles\x18\x03 \x03(\x0b\x32\x1b.Enterprise.ReEncryptedData\x12*\n\x05users\x18\x04 \x03(\x0b\x32\x1b.Enterprise.ReEncryptedData\x12\x30\n\x08roleKeys\x18\x05 \x03(\x0b\x32\x1e.Enterprise.ReEncryptedRoleKey\x12\x35\n\x08teamKeys\x18\x06 \x03(\x0b\x32#.Enterprise.EncryptedTeamKeyRequest\x12\x39\n\rusersDataKeys\x18\x07 \x03(\x0b\x32\".Enterprise.ReEncryptedUserDataKey\",\n\x08RoleTeam\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x0f\n\x07teamUid\x18\x02 \x01(\x0c\"4\n\tRoleTeams\x12\'\n\trole_team\x18\x01 \x03(\x0b\x32\x14.Enterprise.RoleTeam\"R\n\x0fRoleUserAddKeys\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0f\n\x07treeKey\x18\x02 \x01(\t\x12\x14\n\x0croleAdminKey\x18\x03 \x01(\t\"T\n\x0bRoleUserAdd\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x34\n\x0froleUserAddKeys\x18\x02 \x03(\x0b\x32\x1b.Enterprise.RoleUserAddKeys\"D\n\x13RoleUsersAddRequest\x12-\n\x0croleUserAdds\x18\x01 \x03(\x0b\x32\x17.Enterprise.RoleUserAdd\"\x80\x01\n\x11RoleUserAddResult\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x30\n\x06status\x18\x03 \x01(\x0e\x32 .Enterprise.RoleUserModifyStatus\x12\x0f\n\x07message\x18\x04 \x01(\t\"F\n\x14RoleUsersAddResponse\x12.\n\x07results\x18\x01 \x03(\x0b\x32\x1d.Enterprise.RoleUserAddResult\"<\n\x0eRoleUserRemove\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\"M\n\x16RoleUsersRemoveRequest\x12\x33\n\x0froleUserRemoves\x18\x01 \x03(\x0b\x32\x1a.Enterprise.RoleUserRemove\"\x83\x01\n\x14RoleUserRemoveResult\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x30\n\x06status\x18\x03 \x01(\x0e\x32 .Enterprise.RoleUserModifyStatus\x12\x0f\n\x07message\x18\x04 \x01(\t\"L\n\x17RoleUsersRemoveResponse\x12\x31\n\x07results\x18\x01 \x03(\x0b\x32 .Enterprise.RoleUserRemoveResult\"\x86\x04\n\x16\x45nterpriseRegistration\x12\x18\n\x10\x65ncryptedTreeKey\x18\x01 \x01(\x0c\x12\x16\n\x0e\x65nterpriseName\x18\x02 \x01(\t\x12\x14\n\x0crootNodeData\x18\x03 \x01(\x0c\x12\x15\n\radminUserData\x18\x04 \x01(\x0c\x12\x11\n\tadminName\x18\x05 \x01(\t\x12\x10\n\x08roleData\x18\x06 \x01(\x0c\x12\x38\n\nrsaKeyPair\x18\x07 \x01(\x0b\x32$.Enterprise.EnterpriseKeyPairRequest\x12\x13\n\x0bnumberSeats\x18\x08 \x01(\x05\x12\x32\n\x0e\x65nterpriseType\x18\t \x01(\x0e\x32\x1a.Enterprise.EnterpriseType\x12\x15\n\rrolePublicKey\x18\n \x01(\x0c\x12*\n\"rolePrivateKeyEncryptedWithRoleKey\x18\x0b \x01(\x0c\x12#\n\x1broleKeyEncryptedWithTreeKey\x18\x0c \x01(\x0c\x12\x38\n\neccKeyPair\x18\r \x01(\x0b\x32$.Enterprise.EnterpriseKeyPairRequest\x12\x18\n\x10\x61llUsersRoleData\x18\x0e \x01(\x0c\x12)\n!roleKeyEncryptedWithUserPublicKey\x18\x0f \x01(\x0c\"H\n\x1a\x44omainPasswordRulesRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x18\n\x10verificationCode\x18\x02 \x01(\t\"\\\n\x19\x44omainPasswordRulesFields\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07minimum\x18\x02 \x01(\x05\x12\x0f\n\x07maximum\x18\x03 \x01(\x05\x12\x0f\n\x07\x61llowed\x18\x04 \x01(\x08\"E\n\x10LoginToMcRequest\x12\x16\n\x0emcEnterpriseId\x18\x01 \x01(\x05\x12\x19\n\x11messageSessionUid\x18\x02 \x01(\x0c\"L\n\x11LoginToMcResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTreeKey\x18\x02 \x01(\t\"g\n\x1b\x44omainPasswordRulesResponse\x12H\n\x19\x64omainPasswordRulesFields\x18\x01 \x03(\x0b\x32%.Enterprise.DomainPasswordRulesFields\"\x88\x01\n\x18\x41pproveUserDeviceRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x01(\x0c\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x03 \x01(\x0c\x12\x14\n\x0c\x64\x65nyApproval\x18\x04 \x01(\x08\"t\n\x19\x41pproveUserDeviceResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x01(\x0c\x12\x0e\n\x06\x66\x61iled\x18\x03 \x01(\x08\x12\x0f\n\x07message\x18\x04 \x01(\t\"Y\n\x19\x41pproveUserDevicesRequest\x12<\n\x0e\x64\x65viceRequests\x18\x01 \x03(\x0b\x32$.Enterprise.ApproveUserDeviceRequest\"\\\n\x1a\x41pproveUserDevicesResponse\x12>\n\x0f\x64\x65viceResponses\x18\x01 \x03(\x0b\x32%.Enterprise.ApproveUserDeviceResponse\"\x87\x01\n\x15\x45nterpriseUserDataKey\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14userEncryptedDataKey\x18\x02 \x01(\x0c\x12\x11\n\tkeyTypeId\x18\x03 \x01(\x05\x12\x0f\n\x07roleKey\x18\x04 \x01(\x0c\x12\x12\n\nprivateKey\x18\x05 \x01(\x0c\"I\n\x16\x45nterpriseUserDataKeys\x12/\n\x04keys\x18\x01 \x03(\x0b\x32!.Enterprise.EnterpriseUserDataKey\"g\n\x1a\x45nterpriseUserDataKeyLight\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14userEncryptedDataKey\x18\x02 \x01(\x0c\x12\x11\n\tkeyTypeId\x18\x03 \x01(\x05\"d\n\x1c\x45nterpriseUserDataKeysByNode\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x34\n\x04keys\x18\x02 \x03(\x0b\x32&.Enterprise.EnterpriseUserDataKeyLight\"^\n$EnterpriseUserDataKeysByNodeResponse\x12\x36\n\x04keys\x18\x01 \x03(\x0b\x32(.Enterprise.EnterpriseUserDataKeysByNode\"2\n\x15\x45nterpriseDataRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"0\n\x13SpecialProvisioning\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"\x84\x02\n\x11GeneralDataEntity\x12\x16\n\x0e\x65nterpriseName\x18\x01 \x01(\t\x12\x1a\n\x12restrictVisibility\x18\x02 \x01(\x08\x12<\n\x13specialProvisioning\x18\x04 \x01(\x0b\x32\x1f.Enterprise.SpecialProvisioning\x12\x30\n\ruserPrivilege\x18\x07 \x01(\x0b\x32\x19.Enterprise.UserPrivilege\x12\x13\n\x0b\x64istributor\x18\x08 \x01(\x08\x12\x1d\n\x15\x66orbidAccountTransfer\x18\t \x01(\x08\x12\x17\n\x0fshowUserOnboard\x18\n \x01(\x08\"\xfd\x01\n\x04Node\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x10\n\x08parentId\x18\x02 \x01(\x03\x12\x10\n\x08\x62ridgeId\x18\x03 \x01(\x03\x12\x0e\n\x06scimId\x18\x04 \x01(\x03\x12\x11\n\tlicenseId\x18\x05 \x01(\x03\x12\x15\n\rencryptedData\x18\x06 \x01(\t\x12\x12\n\nduoEnabled\x18\x07 \x01(\x08\x12\x12\n\nrsaEnabled\x18\x08 \x01(\x08\x12 \n\x14ssoServiceProviderId\x18\t \x01(\x03\x42\x02\x18\x01\x12\x1a\n\x12restrictVisibility\x18\n \x01(\x08\x12!\n\x15ssoServiceProviderIds\x18\x0b \x03(\x03\x42\x02\x10\x01\"\x8e\x01\n\x04Role\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\t\x12\x0f\n\x07keyType\x18\x04 \x01(\t\x12\x14\n\x0cvisibleBelow\x18\x05 \x01(\x08\x12\x16\n\x0enewUserInherit\x18\x06 \x01(\x08\x12\x10\n\x08roleType\x18\x07 \x01(\t\"\xb8\x02\n\x04User\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\t\x12\x0f\n\x07keyType\x18\x04 \x01(\t\x12\x10\n\x08username\x18\x05 \x01(\t\x12\x0e\n\x06status\x18\x06 \x01(\t\x12\x0c\n\x04lock\x18\x07 \x01(\x05\x12\x0e\n\x06userId\x18\x08 \x01(\x05\x12\x1e\n\x16\x61\x63\x63ountShareExpiration\x18\t \x01(\x03\x12\x10\n\x08\x66ullName\x18\n \x01(\t\x12\x10\n\x08jobTitle\x18\x0b \x01(\t\x12\x12\n\ntfaEnabled\x18\x0c \x01(\x08\x12\x46\n\x18transferAcceptanceStatus\x18\r \x01(\x0e\x32$.Enterprise.TransferAcceptanceStatus\"7\n\tUserAlias\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\"\xac\x01\n\x18\x43omplianceReportMetaData\x12\x11\n\treportUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x12\n\nreportName\x18\x03 \x01(\t\x12\x15\n\rdateGenerated\x18\x04 \x01(\x03\x12\x11\n\trunByName\x18\x05 \x01(\t\x12\x16\n\x0enumberOfOwners\x18\x07 \x01(\x05\x12\x17\n\x0fnumberOfRecords\x18\x08 \x01(\x05\"S\n\x0bManagedNode\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x15\n\rmanagedNodeId\x18\x02 \x01(\x03\x12\x1d\n\x15\x63\x61scadeNodeManagement\x18\x03 \x01(\x08\"T\n\x0fUserManagedNode\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x1d\n\x15\x63\x61scadeNodeManagement\x18\x02 \x01(\x08\x12\x12\n\nprivileges\x18\x03 \x03(\t\"w\n\rUserPrivilege\x12\x35\n\x10userManagedNodes\x18\x01 \x03(\x0b\x32\x1b.Enterprise.UserManagedNode\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\t\"4\n\x08RoleUser\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"M\n\rRolePrivilege\x12\x15\n\rmanagedNodeId\x18\x01 \x01(\x03\x12\x0e\n\x06roleId\x18\x02 \x01(\x03\x12\x15\n\rprivilegeType\x18\x03 \x01(\t\"I\n\x0fRoleEnforcement\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x17\n\x0f\x65nforcementType\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\"\xa9\x01\n\x04Team\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x14\n\x0crestrictEdit\x18\x04 \x01(\x08\x12\x15\n\rrestrictShare\x18\x05 \x01(\x08\x12\x14\n\x0crestrictView\x18\x06 \x01(\x08\x12\x15\n\rencryptedData\x18\x07 \x01(\t\x12\x18\n\x10\x65ncryptedTeamKey\x18\x08 \x01(\t\"G\n\x08TeamUser\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x10\n\x08userType\x18\x03 \x01(\t\"K\n\x1aGetDistributorInfoResponse\x12-\n\x0c\x64istributors\x18\x01 \x03(\x0b\x32\x17.Enterprise.Distributor\"B\n\x0b\x44istributor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12%\n\x08mspInfos\x18\x02 \x03(\x0b\x32\x13.Enterprise.MspInfo\"\x9d\x02\n\x07MspInfo\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x16\n\x0e\x65nterpriseName\x18\x02 \x01(\t\x12\x19\n\x11\x61llocatedLicenses\x18\x03 \x01(\x05\x12\x19\n\x11\x61llowedMcProducts\x18\x04 \x03(\t\x12\x15\n\rallowedAddOns\x18\x05 \x03(\t\x12\x17\n\x0fmaxFilePlanType\x18\x06 \x01(\t\x12\x34\n\x10managedCompanies\x18\x07 \x03(\x0b\x32\x1a.Enterprise.ManagedCompany\x12\x1e\n\x16\x61llowUnlimitedLicenses\x18\x08 \x01(\x08\x12(\n\x06\x61\x64\x64Ons\x18\t \x03(\x0b\x32\x18.Enterprise.LicenseAddOn\"\x91\x02\n\x0eManagedCompany\x12\x16\n\x0emcEnterpriseId\x18\x01 \x01(\x05\x12\x18\n\x10mcEnterpriseName\x18\x02 \x01(\t\x12\x11\n\tmspNodeId\x18\x03 \x01(\x03\x12\x15\n\rnumberOfSeats\x18\x04 \x01(\x05\x12\x15\n\rnumberOfUsers\x18\x05 \x01(\x05\x12\x11\n\tproductId\x18\x06 \x01(\t\x12\x11\n\tisExpired\x18\x07 \x01(\x08\x12\x0f\n\x07treeKey\x18\x08 \x01(\t\x12\x15\n\rtree_key_role\x18\t \x01(\x03\x12\x14\n\x0c\x66ilePlanType\x18\n \x01(\t\x12(\n\x06\x61\x64\x64Ons\x18\x0b \x03(\x0b\x32\x18.Enterprise.LicenseAddOn\"R\n\x07MSPPool\x12\x11\n\tproductId\x18\x01 \x01(\t\x12\r\n\x05seats\x18\x02 \x01(\x05\x12\x16\n\x0e\x61vailableSeats\x18\x03 \x01(\x05\x12\r\n\x05stash\x18\x04 \x01(\x05\":\n\nMSPContact\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x16\n\x0e\x65nterpriseName\x18\x02 \x01(\t\"\xec\x01\n\x0cLicenseAddOn\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x02 \x01(\x08\x12\x0f\n\x07isTrial\x18\x03 \x01(\x08\x12\x12\n\nexpiration\x18\x04 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\r\n\x05seats\x18\x06 \x01(\x05\x12\x16\n\x0e\x61\x63tivationTime\x18\x07 \x01(\x03\x12\x19\n\x11includedInProduct\x18\x08 \x01(\x08\x12\x14\n\x0c\x61piCallCount\x18\t \x01(\x05\x12\x17\n\x0ftierDescription\x18\n \x01(\t\x12\x16\n\x0eseatsAllocated\x18\x0b \x01(\x05\"s\n\tMCDefault\x12\x11\n\tmcProduct\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x64\x64Ons\x18\x02 \x03(\t\x12\x14\n\x0c\x66ilePlanType\x18\x03 \x01(\t\x12\x13\n\x0bmaxLicenses\x18\x04 \x01(\x05\x12\x18\n\x10\x66ixedMaxLicenses\x18\x05 \x01(\x08\"\xd2\x01\n\nMSPPermits\x12\x12\n\nrestricted\x18\x01 \x01(\x08\x12\x1a\n\x12maxAllowedLicenses\x18\x02 \x01(\x05\x12\x19\n\x11\x61llowedMcProducts\x18\x03 \x03(\t\x12\x15\n\rallowedAddOns\x18\x04 \x03(\t\x12\x17\n\x0fmaxFilePlanType\x18\x05 \x01(\t\x12\x1e\n\x16\x61llowUnlimitedLicenses\x18\x06 \x01(\x08\x12)\n\nmcDefaults\x18\x07 \x03(\x0b\x32\x15.Enterprise.MCDefault\"\xa0\x04\n\x07License\x12\x0c\n\x04paid\x18\x01 \x01(\x08\x12\x15\n\rnumberOfSeats\x18\x02 \x01(\x05\x12\x12\n\nexpiration\x18\x03 \x01(\x03\x12\x14\n\x0clicenseKeyId\x18\x04 \x01(\x05\x12\x15\n\rproductTypeId\x18\x05 \x01(\x05\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x1b\n\x13\x65nterpriseLicenseId\x18\x07 \x01(\x03\x12\x16\n\x0eseatsAllocated\x18\x08 \x01(\x05\x12\x14\n\x0cseatsPending\x18\t \x01(\x05\x12\x0c\n\x04tier\x18\n \x01(\x05\x12\x16\n\x0e\x66ilePlanTypeId\x18\x0b \x01(\x05\x12\x10\n\x08maxBytes\x18\x0c \x01(\x03\x12\x19\n\x11storageExpiration\x18\r \x01(\x03\x12\x15\n\rlicenseStatus\x18\x0e \x01(\t\x12$\n\x07mspPool\x18\x0f \x03(\x0b\x32\x13.Enterprise.MSPPool\x12)\n\tmanagedBy\x18\x10 \x01(\x0b\x32\x16.Enterprise.MSPContact\x12(\n\x06\x61\x64\x64Ons\x18\x11 \x03(\x0b\x32\x18.Enterprise.LicenseAddOn\x12\x17\n\x0fnextBillingDate\x18\x12 \x01(\x03\x12\x17\n\x0fhasMSPLegacyLog\x18\x13 \x01(\x08\x12*\n\nmspPermits\x18\x14 \x01(\x0b\x32\x16.Enterprise.MSPPermits\x12\x13\n\x0b\x64istributor\x18\x15 \x01(\x08\"n\n\x06\x42ridge\x12\x10\n\x08\x62ridgeId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x18\n\x10wanIpEnforcement\x18\x03 \x01(\t\x12\x18\n\x10lanIpEnforcement\x18\x04 \x01(\t\x12\x0e\n\x06status\x18\x05 \x01(\t\"t\n\x04Scim\x12\x0e\n\x06scimId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\nlastSynced\x18\x04 \x01(\x03\x12\x12\n\nrolePrefix\x18\x05 \x01(\t\x12\x14\n\x0cuniqueGroups\x18\x06 \x01(\x08\"L\n\x0e\x45mailProvision\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0e\n\x06\x64omain\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\"R\n\nQueuedTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x15\n\rencryptedData\x18\x04 \x01(\t\"0\n\x0eQueuedTeamUser\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\r\n\x05users\x18\x02 \x03(\x03\"\xa4\x01\n\x0eTeamsAddResult\x12\x34\n\x11successfulTeamAdd\x18\x01 \x03(\x0b\x32\x19.Enterprise.TeamAddResult\x12\x36\n\x13unsuccessfulTeamAdd\x18\x02 \x03(\x0b\x32\x19.Enterprise.TeamAddResult\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\x14\n\x0c\x65rrorMessage\x18\x04 \x01(\t\"U\n\rTeamAddResult\x12\x1e\n\x04team\x18\x01 \x01(\x0b\x32\x10.Enterprise.Team\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x01(\t\"\x91\x01\n\nSsoService\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06sp_url\x18\x04 \x01(\t\x12\x16\n\x0einviteNewUsers\x18\x05 \x01(\x08\x12\x0e\n\x06\x61\x63tive\x18\x06 \x01(\x08\x12\x0f\n\x07isCloud\x18\x07 \x01(\x08\"1\n\x10ReportFilterUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\r\n\x05\x65mail\x18\x02 \x01(\t\"\x97\x02\n\x1d\x44\x65viceRequestForAdminApproval\x12\x10\n\x08\x64\x65viceId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x03 \x01(\x0c\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12\x12\n\ndeviceName\x18\x05 \x01(\t\x12\x15\n\rclientVersion\x18\x06 \x01(\t\x12\x12\n\ndeviceType\x18\x07 \x01(\t\x12\x0c\n\x04\x64\x61te\x18\x08 \x01(\x03\x12\x11\n\tipAddress\x18\t \x01(\t\x12\x10\n\x08location\x18\n \x01(\t\x12\r\n\x05\x65mail\x18\x0b \x01(\t\x12\x12\n\naccountUid\x18\x0c \x01(\x0c\"`\n\x0e\x45nterpriseData\x12\x30\n\x06\x65ntity\x18\x01 \x01(\x0e\x32 .Enterprise.EnterpriseDataEntity\x12\x0e\n\x06\x64\x65lete\x18\x02 \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x03 \x03(\x0c\"\xd0\x01\n\x16\x45nterpriseDataResponse\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\x12,\n\x0b\x63\x61\x63heStatus\x18\x03 \x01(\x0e\x32\x17.Enterprise.CacheStatus\x12(\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x1a.Enterprise.EnterpriseData\x12\x32\n\x0bgeneralData\x18\x05 \x01(\x0b\x32\x1d.Enterprise.GeneralDataEntity\"*\n\rBackupRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"\x98\x01\n\x0c\x42\x61\x63kupRecord\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12*\n\x07keyType\x18\x04 \x01(\x0e\x32\x19.Enterprise.BackupKeyType\x12\x0f\n\x07version\x18\x05 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\r\n\x05\x65xtra\x18\x07 \x01(\x0c\".\n\tBackupKey\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\tbackupKey\x18\x02 \x01(\x0c\"\x8d\x02\n\nBackupUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x10\n\x08userName\x18\x02 \x01(\t\x12\x0f\n\x07\x64\x61taKey\x18\x03 \x01(\x0c\x12\x36\n\x0b\x64\x61taKeyType\x18\x04 \x01(\x0e\x32!.Enterprise.BackupUserDataKeyType\x12\x12\n\nprivateKey\x18\x05 \x01(\x0c\x12\x0f\n\x07treeKey\x18\x06 \x01(\x0c\x12.\n\x0btreeKeyType\x18\x07 \x01(\x0e\x32\x19.Enterprise.BackupKeyType\x12)\n\nbackupKeys\x18\x08 \x03(\x0b\x32\x15.Enterprise.BackupKey\x12\x14\n\x0cprivateECKey\x18\t \x01(\x0c\"\x9e\x01\n\x0e\x42\x61\x63kupResponse\x12\x1f\n\x17\x65nterpriseEccPrivateKey\x18\x01 \x01(\x0c\x12%\n\x05users\x18\x02 \x03(\x0b\x32\x16.Enterprise.BackupUser\x12)\n\x07records\x18\x03 \x03(\x0b\x32\x18.Enterprise.BackupRecord\x12\x19\n\x11\x63ontinuationToken\x18\x04 \x01(\x0c\"e\n\nBackupFile\x12\x0c\n\x04user\x18\x01 \x01(\t\x12\x11\n\tbackupUid\x18\x02 \x01(\x0c\x12\x10\n\x08\x66ileName\x18\x03 \x01(\t\x12\x0f\n\x07\x63reated\x18\x04 \x01(\x03\x12\x13\n\x0b\x64ownloadUrl\x18\x05 \x01(\t\"8\n\x0f\x42\x61\x63kupsResponse\x12%\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x16.Enterprise.BackupFile\".\n\x1cGetEnterpriseDataKeysRequest\x12\x0e\n\x06roleId\x18\x01 \x03(\x03\"\xff\x01\n\x1dGetEnterpriseDataKeysResponse\x12:\n\x12reEncryptedRoleKey\x18\x01 \x03(\x0b\x32\x1e.Enterprise.ReEncryptedRoleKey\x12$\n\x07roleKey\x18\x02 \x03(\x0b\x32\x13.Enterprise.RoleKey\x12\"\n\x06mspKey\x18\x03 \x01(\x0b\x32\x12.Enterprise.MspKey\x12\x32\n\x0e\x65nterpriseKeys\x18\x04 \x01(\x0b\x32\x1a.Enterprise.EnterpriseKeys\x12$\n\x07treeKey\x18\x05 \x01(\x0b\x32\x13.Enterprise.TreeKey\"^\n\x07RoleKey\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x14\n\x0c\x65ncryptedKey\x18\x02 \x01(\t\x12-\n\x07keyType\x18\x03 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"d\n\x06MspKey\x12\x1b\n\x13\x65ncryptedMspTreeKey\x18\x01 \x01(\t\x12=\n\x17\x65ncryptedMspTreeKeyType\x18\x02 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"|\n\x0e\x45nterpriseKeys\x12\x14\n\x0crsaPublicKey\x18\x01 \x01(\x0c\x12\x1e\n\x16rsaEncryptedPrivateKey\x18\x02 \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\x03 \x01(\x0c\x12\x1e\n\x16\x65\x63\x63\x45ncryptedPrivateKey\x18\x04 \x01(\x0c\"H\n\x07TreeKey\x12\x0f\n\x07treeKey\x18\x01 \x01(\t\x12,\n\tkeyTypeId\x18\x02 \x01(\x0e\x32\x19.Enterprise.BackupKeyType\"E\n\x14SharedRecordResponse\x12-\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1d.Enterprise.SharedRecordEvent\"p\n\x11SharedRecordEvent\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08userName\x18\x02 \x01(\t\x12\x0f\n\x07\x63\x61nEdit\x18\x03 \x01(\x08\x12\x12\n\ncanReshare\x18\x04 \x01(\x08\x12\x11\n\tshareFrom\x18\x05 \x01(\x05\".\n\x1cSetRestrictVisibilityRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\"\xd0\x01\n\x0eUserAddRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12-\n\x07keyType\x18\x04 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x05 \x01(\t\x12\x10\n\x08jobTitle\x18\x06 \x01(\t\x12\r\n\x05\x65mail\x18\x07 \x01(\t\x12\x1b\n\x13suppressEmailInvite\x18\x08 \x01(\x08\":\n\x11UserUpdateRequest\x12%\n\x05users\x18\x01 \x03(\x0b\x32\x16.Enterprise.UserUpdate\"\xaf\x01\n\nUserUpdate\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12-\n\x07keyType\x18\x04 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x05 \x01(\t\x12\x10\n\x08jobTitle\x18\x06 \x01(\t\x12\r\n\x05\x65mail\x18\x07 \x01(\t\"A\n\x12UserUpdateResponse\x12+\n\x05users\x18\x01 \x03(\x0b\x32\x1c.Enterprise.UserUpdateResult\"Z\n\x10UserUpdateResult\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12,\n\x06status\x18\x02 \x01(\x0e\x32\x1c.Enterprise.UserUpdateStatus\"J\n\x1d\x43omplianceRecordOwnersRequest\x12\x0f\n\x07nodeIds\x18\x01 \x03(\x03\x12\x18\n\x10includeNonShared\x18\x02 \x01(\x08\"O\n\x1e\x43omplianceRecordOwnersResponse\x12-\n\x0crecordOwners\x18\x01 \x03(\x0b\x32\x17.Enterprise.RecordOwner\"7\n\x0bRecordOwner\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06shared\x18\x02 \x01(\x08\"\xa6\x01\n PreliminaryComplianceDataRequest\x12\x19\n\x11\x65nterpriseUserIds\x18\x01 \x03(\x03\x12\x18\n\x10includeNonShared\x18\x02 \x01(\x08\x12\x19\n\x11\x63ontinuationToken\x18\x03 \x01(\x0c\x12\x32\n*includeTotalMatchingRecordsInFirstResponse\x18\x04 \x01(\x08\"\x9f\x01\n!PreliminaryComplianceDataResponse\x12\x30\n\rauditUserData\x18\x01 \x03(\x0b\x32\x19.Enterprise.AuditUserData\x12\x19\n\x11\x63ontinuationToken\x18\x02 \x01(\x0c\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\x12\x1c\n\x14totalMatchingRecords\x18\x04 \x01(\x05\"K\n\x0f\x41uditUserRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x15\n\rencryptedData\x18\x02 \x01(\x0c\x12\x0e\n\x06shared\x18\x03 \x01(\x08\"\x8d\x01\n\rAuditUserData\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x35\n\x10\x61uditUserRecords\x18\x02 \x03(\x0b\x32\x1b.Enterprise.AuditUserRecord\x12+\n\x06status\x18\x03 \x01(\x0e\x32\x1b.Enterprise.AuditUserStatus\"\x7f\n\x17\x43omplianceReportFilters\x12\x14\n\x0crecordTitles\x18\x01 \x03(\t\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\x12\x11\n\tjobTitles\x18\x03 \x03(\x03\x12\x0c\n\x04urls\x18\x04 \x03(\t\x12\x19\n\x11\x65nterpriseUserIds\x18\x05 \x03(\x03\"\x7f\n\x17\x43omplianceReportRequest\x12<\n\x13\x63omplianceReportRun\x18\x01 \x01(\x0b\x32\x1f.Enterprise.ComplianceReportRun\x12\x12\n\nreportName\x18\x02 \x01(\t\x12\x12\n\nsaveReport\x18\x03 \x01(\x08\"\x85\x01\n\x13\x43omplianceReportRun\x12N\n\x17reportCriteriaAndFilter\x18\x01 \x01(\x0b\x32-.Enterprise.ComplianceReportCriteriaAndFilter\x12\r\n\x05users\x18\x02 \x03(\x03\x12\x0f\n\x07records\x18\x03 \x03(\x0c\"\xfc\x01\n!ComplianceReportCriteriaAndFilter\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x13\n\x0b\x63riteriaUid\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63riteriaName\x18\x03 \x01(\t\x12\x36\n\x08\x63riteria\x18\x04 \x01(\x0b\x32$.Enterprise.ComplianceReportCriteria\x12\x33\n\x07\x66ilters\x18\x05 \x03(\x0b\x32\".Enterprise.ComplianceReportFilter\x12\x14\n\x0clastModified\x18\x06 \x01(\x03\x12\x19\n\x11nodeEncryptedData\x18\x07 \x01(\x0c\"b\n\x18\x43omplianceReportCriteria\x12\x11\n\tjobTitles\x18\x01 \x03(\t\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\x12\x18\n\x10includeNonShared\x18\x03 \x01(\x08\"x\n\x16\x43omplianceReportFilter\x12\x14\n\x0crecordTitles\x18\x01 \x03(\t\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\x12\x11\n\tjobTitles\x18\x03 \x03(\t\x12\x0c\n\x04urls\x18\x04 \x03(\t\x12\x13\n\x0brecordTypes\x18\x05 \x03(\t\"\xa1\x05\n\x18\x43omplianceReportResponse\x12\x15\n\rdateGenerated\x18\x01 \x01(\x03\x12\x15\n\rrunByUserName\x18\x02 \x01(\t\x12\x12\n\nreportName\x18\x03 \x01(\t\x12\x11\n\treportUid\x18\x04 \x01(\x0c\x12<\n\x13\x63omplianceReportRun\x18\x05 \x01(\x0b\x32\x1f.Enterprise.ComplianceReportRun\x12-\n\x0cuserProfiles\x18\x06 \x03(\x0b\x32\x17.Enterprise.UserProfile\x12)\n\nauditTeams\x18\x07 \x03(\x0b\x32\x15.Enterprise.AuditTeam\x12-\n\x0c\x61uditRecords\x18\x08 \x03(\x0b\x32\x17.Enterprise.AuditRecord\x12+\n\x0buserRecords\x18\t \x03(\x0b\x32\x16.Enterprise.UserRecord\x12;\n\x13sharedFolderRecords\x18\n \x03(\x0b\x32\x1e.Enterprise.SharedFolderRecord\x12\x37\n\x11sharedFolderUsers\x18\x0b \x03(\x0b\x32\x1c.Enterprise.SharedFolderUser\x12\x37\n\x11sharedFolderTeams\x18\x0c \x03(\x0b\x32\x1c.Enterprise.SharedFolderTeam\x12\x31\n\x0e\x61uditTeamUsers\x18\r \x03(\x0b\x32\x19.Enterprise.AuditTeamUser\x12)\n\nauditRoles\x18\x0e \x03(\x0b\x32\x15.Enterprise.AuditRole\x12/\n\rlinkedRecords\x18\x0f \x03(\x0b\x32\x18.Enterprise.LinkedRecord\"\x81\x01\n\x0b\x41uditRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x11\n\tauditData\x18\x02 \x01(\x0c\x12\x16\n\x0ehasAttachments\x18\x03 \x01(\x08\x12\x0f\n\x07inTrash\x18\x04 \x01(\x08\x12\x10\n\x08treeLeft\x18\x05 \x01(\x05\x12\x11\n\ttreeRight\x18\x06 \x01(\x05\"\x80\x02\n\tAuditRole\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x15\n\rencryptedData\x18\x02 \x01(\x0c\x12&\n\x1erestrictShareOutsideEnterprise\x18\x03 \x01(\x08\x12\x18\n\x10restrictShareAll\x18\x04 \x01(\x08\x12\"\n\x1arestrictShareOfAttachments\x18\x05 \x01(\x08\x12)\n!restrictMaskPasswordsWhileEditing\x18\x06 \x01(\x08\x12;\n\x13roleNodeManagements\x18\x07 \x03(\x0b\x32\x1e.Enterprise.RoleNodeManagement\"^\n\x12RoleNodeManagement\x12\x10\n\x08treeLeft\x18\x01 \x01(\x05\x12\x11\n\ttreeRight\x18\x02 \x01(\x05\x12\x0f\n\x07\x63\x61scade\x18\x03 \x01(\x08\x12\x12\n\nprivileges\x18\x04 \x01(\x05\"k\n\x0bUserProfile\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08\x66ullName\x18\x02 \x01(\t\x12\x10\n\x08jobTitle\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x0f\n\x07roleIds\x18\x05 \x03(\x03\"=\n\x10RecordPermission\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x16\n\x0epermissionBits\x18\x02 \x01(\x05\"_\n\nUserRecord\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x37\n\x11recordPermissions\x18\x02 \x03(\x0b\x32\x1c.Enterprise.RecordPermission\"[\n\tAuditTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x10\n\x08teamName\x18\x02 \x01(\t\x12\x14\n\x0crestrictEdit\x18\x03 \x01(\x08\x12\x15\n\rrestrictShare\x18\x04 \x01(\x08\";\n\rAuditTeamUser\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\"\x9f\x01\n\x12SharedFolderRecord\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x37\n\x11recordPermissions\x18\x02 \x03(\x0b\x32\x1c.Enterprise.RecordPermission\x12\x37\n\x11shareAdminRecords\x18\x03 \x03(\x0b\x32\x1c.Enterprise.ShareAdminRecord\"M\n\x10ShareAdminRecord\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1f\n\x17recordPermissionIndexes\x18\x02 \x03(\x05\"F\n\x10SharedFolderUser\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\"=\n\x10SharedFolderTeam\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08teamUids\x18\x02 \x03(\x0c\"/\n\x1aGetComplianceReportRequest\x12\x11\n\treportUid\x18\x01 \x01(\x0c\"2\n\x1bGetComplianceReportResponse\x12\x13\n\x0b\x64ownloadUrl\x18\x01 \x01(\t\"6\n\x1f\x43omplianceReportCriteriaRequest\x12\x13\n\x0b\x63riteriaUid\x18\x01 \x01(\x0c\";\n$SaveComplianceReportCriteriaResponse\x12\x13\n\x0b\x63riteriaUid\x18\x01 \x01(\x0c\"4\n\x0cLinkedRecord\x12\x10\n\x08ownerUid\x18\x01 \x01(\x0c\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\"W\n\x17GetSharingAdminsRequest\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x10\n\x08username\x18\x03 \x01(\t\"\xe0\x01\n\x0eUserProfileExt\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x10\n\x08\x66ullName\x18\x02 \x01(\t\x12\x10\n\x08jobTitle\x18\x03 \x01(\t\x12\x14\n\x0cisMSPMCAdmin\x18\x04 \x01(\x08\x12\x18\n\x10isInSharedFolder\x18\x05 \x01(\x08\x12&\n\x1eisShareAdminForRequestedObject\x18\x06 \x01(\x08\x12(\n isShareAdminForSharedFolderOwner\x18\x07 \x01(\x08\x12\x19\n\x11hasAccessToObject\x18\x08 \x01(\x08\"O\n\x18GetSharingAdminsResponse\x12\x33\n\x0fuserProfileExts\x18\x01 \x03(\x0b\x32\x1a.Enterprise.UserProfileExt\"_\n\x1eTeamsEnterpriseUsersAddRequest\x12=\n\x05teams\x18\x01 \x03(\x0b\x32..Enterprise.TeamsEnterpriseUsersAddTeamRequest\"t\n\"TeamsEnterpriseUsersAddTeamRequest\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12=\n\x05users\x18\x02 \x03(\x0b\x32..Enterprise.TeamsEnterpriseUsersAddUserRequest\"\xab\x01\n\"TeamsEnterpriseUsersAddUserRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12*\n\x08userType\x18\x02 \x01(\x0e\x32\x18.Enterprise.TeamUserType\x12\x13\n\x07teamKey\x18\x03 \x01(\tB\x02\x18\x01\x12*\n\x0ctypedTeamKey\x18\x04 \x01(\x0b\x32\x14.Enterprise.TypedKey\"F\n\x08TypedKey\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12-\n\x07keyType\x18\x02 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"s\n\x1fTeamsEnterpriseUsersAddResponse\x12>\n\x05teams\x18\x01 \x03(\x0b\x32/.Enterprise.TeamsEnterpriseUsersAddTeamResponse\x12\x10\n\x08revision\x18\x02 \x01(\x03\"\xc4\x01\n#TeamsEnterpriseUsersAddTeamResponse\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12>\n\x05users\x18\x02 \x03(\x0b\x32/.Enterprise.TeamsEnterpriseUsersAddUserResponse\x12\x0f\n\x07success\x18\x03 \x01(\x08\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x12\n\nresultCode\x18\x05 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x06 \x01(\t\"\x9f\x01\n#TeamsEnterpriseUsersAddUserResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0f\n\x07success\x18\x03 \x01(\x08\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x12\n\nresultCode\x18\x05 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x06 \x01(\t\"M\n\x0b\x44omainAlias\x12\x0e\n\x06\x64omain\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x0f\n\x07message\x18\x04 \x01(\t\"B\n\x12\x44omainAliasRequest\x12,\n\x0b\x64omainAlias\x18\x01 \x03(\x0b\x32\x17.Enterprise.DomainAlias\"C\n\x13\x44omainAliasResponse\x12,\n\x0b\x64omainAlias\x18\x01 \x03(\x0b\x32\x17.Enterprise.DomainAlias\"m\n\x1f\x45nterpriseUsersProvisionRequest\x12\x33\n\x05users\x18\x01 \x03(\x0b\x32$.Enterprise.EnterpriseUsersProvision\x12\x15\n\rclientVersion\x18\x02 \x01(\t\"\xb6\x03\n\x18\x45nterpriseUsersProvision\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x15\n\rencryptedData\x18\x04 \x01(\t\x12-\n\x07keyType\x18\x05 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x06 \x01(\t\x12\x10\n\x08jobTitle\x18\x07 \x01(\t\x12\x1e\n\x16\x65nterpriseUsersDataKey\x18\x08 \x01(\x0c\x12\x14\n\x0c\x61uthVerifier\x18\t \x01(\x0c\x12\x18\n\x10\x65ncryptionParams\x18\n \x01(\x0c\x12\x14\n\x0crsaPublicKey\x18\x0b \x01(\x0c\x12\x1e\n\x16rsaEncryptedPrivateKey\x18\x0c \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\r \x01(\x0c\x12\x1e\n\x16\x65\x63\x63\x45ncryptedPrivateKey\x18\x0e \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x0f \x01(\x0c\x12\x1a\n\x12\x65ncryptedClientKey\x18\x10 \x01(\x0c\"_\n EnterpriseUsersProvisionResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.Enterprise.EnterpriseUsersProvisionResult\"q\n\x1e\x45nterpriseUsersProvisionResult\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x04 \x01(\t\"a\n\x19\x45nterpriseUsersAddRequest\x12-\n\x05users\x18\x01 \x03(\x0b\x32\x1e.Enterprise.EnterpriseUsersAdd\x12\x15\n\rclientVersion\x18\x02 \x01(\t\"\x8c\x02\n\x12\x45nterpriseUsersAdd\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x15\n\rencryptedData\x18\x04 \x01(\t\x12-\n\x07keyType\x18\x05 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x06 \x01(\t\x12\x10\n\x08jobTitle\x18\x07 \x01(\t\x12\x1b\n\x13suppressEmailInvite\x18\x08 \x01(\x08\x12\x15\n\rinviteeLocale\x18\t \x01(\t\x12\x0c\n\x04move\x18\n \x01(\x08\x12\x0e\n\x06roleId\x18\x0b \x01(\x03\"\x9b\x01\n\x1a\x45nterpriseUsersAddResponse\x12\x35\n\x07results\x18\x01 \x03(\x0b\x32$.Enterprise.EnterpriseUsersAddResult\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x05 \x01(\t\"\x96\x01\n\x18\x45nterpriseUsersAddResult\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x18\n\x10verificationCode\x18\x03 \x01(\t\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\x12\x0f\n\x07message\x18\x05 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x06 \x01(\t\"\xb9\x01\n\x17UpdateMSPPermitsRequest\x12\x17\n\x0fmspEnterpriseId\x18\x01 \x01(\x05\x12\x1a\n\x12maxAllowedLicenses\x18\x02 \x01(\x05\x12\x19\n\x11\x61llowedMcProducts\x18\x03 \x03(\t\x12\x15\n\rallowedAddOns\x18\x04 \x03(\t\x12\x17\n\x0fmaxFilePlanType\x18\x05 \x01(\t\x12\x1e\n\x16\x61llowUnlimitedLicenses\x18\x06 \x01(\x08\"9\n\x1c\x44\x65leteEnterpriseUsersRequest\x12\x19\n\x11\x65nterpriseUserIds\x18\x01 \x03(\x03\"o\n\x1a\x44\x65leteEnterpriseUserStatus\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x37\n\x06status\x18\x02 \x01(\x0e\x32\'.Enterprise.DeleteEnterpriseUsersResult\"]\n\x1d\x44\x65leteEnterpriseUsersResponse\x12<\n\x0c\x64\x65leteStatus\x18\x01 \x03(\x0b\x32&.Enterprise.DeleteEnterpriseUserStatus\"w\n\x18\x43learSecurityDataRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x03(\x03\x12\x10\n\x08\x61llUsers\x18\x02 \x01(\x08\x12/\n\x04type\x18\x03 \x01(\x0e\x32!.Enterprise.ClearSecurityDataType*\x1b\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x07\n\x03\x45\x43\x43\x10\x01*\xe6\x01\n\x14RoleUserModifyStatus\x12\x0f\n\x0bROLE_EXISTS\x10\x00\x12\x14\n\x10MISSING_TREE_KEY\x10\x01\x12\x14\n\x10MISSING_ROLE_KEY\x10\x02\x12\x1e\n\x1aINVALID_ENTERPRISE_USER_ID\x10\x03\x12\x1b\n\x17PENDING_ENTERPRISE_USER\x10\x04\x12\x13\n\x0fINVALID_NODE_ID\x10\x05\x12!\n\x1dMAY_NOT_REMOVE_SELF_FROM_ROLE\x10\x06\x12\x1c\n\x18MUST_HAVE_ONE_USER_ADMIN\x10\x07*=\n\x0e\x45nterpriseType\x12\x17\n\x13\x45NTERPRISE_STANDARD\x10\x00\x12\x12\n\x0e\x45NTERPRISE_MSP\x10\x01*s\n\x18TransferAcceptanceStatus\x12\r\n\tUNDEFINED\x10\x00\x12\x10\n\x0cNOT_REQUIRED\x10\x01\x12\x10\n\x0cNOT_ACCEPTED\x10\x02\x12\x16\n\x12PARTIALLY_ACCEPTED\x10\x03\x12\x0c\n\x08\x41\x43\x43\x45PTED\x10\x04*\x8a\x04\n\x14\x45nterpriseDataEntity\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05NODES\x10\x01\x12\t\n\x05ROLES\x10\x02\x12\t\n\x05USERS\x10\x03\x12\t\n\x05TEAMS\x10\x04\x12\x0e\n\nTEAM_USERS\x10\x05\x12\x0e\n\nROLE_USERS\x10\x06\x12\x13\n\x0fROLE_PRIVILEGES\x10\x07\x12\x15\n\x11ROLE_ENFORCEMENTS\x10\x08\x12\x0e\n\nROLE_TEAMS\x10\t\x12\x0c\n\x08LICENSES\x10\n\x12\x11\n\rMANAGED_NODES\x10\x0b\x12\x15\n\x11MANAGED_COMPANIES\x10\x0c\x12\x0b\n\x07\x42RIDGES\x10\r\x12\t\n\x05SCIMS\x10\x0e\x12\x13\n\x0f\x45MAIL_PROVISION\x10\x0f\x12\x10\n\x0cQUEUED_TEAMS\x10\x10\x12\x15\n\x11QUEUED_TEAM_USERS\x10\x11\x12\x10\n\x0cSSO_SERVICES\x10\x12\x12\x17\n\x13REPORT_FILTER_USERS\x10\x13\x12&\n\"DEVICES_REQUEST_FOR_ADMIN_APPROVAL\x10\x14\x12\x10\n\x0cUSER_ALIASES\x10\x15\x12)\n%COMPLIANCE_REPORT_CRITERIA_AND_FILTER\x10\x16\x12\x16\n\x12\x43OMPLIANCE_REPORTS\x10\x17\x12\'\n#QUEUED_TEAM_USERS_INCLUDING_PENDING\x10\x18*\"\n\x0b\x43\x61\x63heStatus\x12\x08\n\x04KEEP\x10\x00\x12\t\n\x05\x43LEAR\x10\x01*\x93\x01\n\rBackupKeyType\x12\n\n\x06NO_KEY\x10\x00\x12\x19\n\x15\x45NCRYPTED_BY_DATA_KEY\x10\x01\x12\x1b\n\x17\x45NCRYPTED_BY_PUBLIC_KEY\x10\x02\x12\x1d\n\x19\x45NCRYPTED_BY_DATA_KEY_GCM\x10\x03\x12\x1f\n\x1b\x45NCRYPTED_BY_PUBLIC_KEY_ECC\x10\x04*:\n\x15\x42\x61\x63kupUserDataKeyType\x12\x07\n\x03OWN\x10\x00\x12\x18\n\x14SHARED_TO_ENTERPRISE\x10\x01*\xa5\x01\n\x10\x45ncryptedKeyType\x12\r\n\tKT_NO_KEY\x10\x00\x12\x1c\n\x18KT_ENCRYPTED_BY_DATA_KEY\x10\x01\x12\x1e\n\x1aKT_ENCRYPTED_BY_PUBLIC_KEY\x10\x02\x12 \n\x1cKT_ENCRYPTED_BY_DATA_KEY_GCM\x10\x03\x12\"\n\x1eKT_ENCRYPTED_BY_PUBLIC_KEY_ECC\x10\x04*\x8e\x02\n\x12\x45nterpriseFlagType\x12\x0b\n\x07INVALID\x10\x00\x12\x1a\n\x16\x41LLOW_PERSONAL_LICENSE\x10\x01\x12\x18\n\x14SPECIAL_PROVISIONING\x10\x02\x12\x10\n\x0cRECORD_TYPES\x10\x03\x12\x13\n\x0fSECRETS_MANAGER\x10\x04\x12\x15\n\x11\x45NTERPRISE_LOCKED\x10\x05\x12\x15\n\x11\x46ORBID_KEY_TYPE_2\x10\x06\x12\x15\n\x11\x43ONSOLE_ONBOARDED\x10\x07\x12\x1b\n\x17\x46ORBID_ACCOUNT_TRANSFER\x10\x08\x12\x15\n\x11NPS_POPUP_OPT_OUT\x10\t\x12\x15\n\x11SHOW_USER_ONBOARD\x10\n*E\n\x10UserUpdateStatus\x12\x12\n\x0eUSER_UPDATE_OK\x10\x00\x12\x1d\n\x19USER_UPDATE_ACCESS_DENIED\x10\x01*I\n\x0f\x41uditUserStatus\x12\x06\n\x02OK\x10\x00\x12\x11\n\rACCESS_DENIED\x10\x01\x12\x1b\n\x17NO_LONGER_IN_ENTERPRISE\x10\x02*3\n\x0cTeamUserType\x12\x08\n\x04USER\x10\x00\x12\t\n\x05\x41\x44MIN\x10\x01\x12\x0e\n\nADMIN_ONLY\x10\x02*x\n\rAppClientType\x12\x0c\n\x08NOT_USED\x10\x00\x12\x0b\n\x07GENERAL\x10\x01\x12%\n!DISCOVERY_AND_ROTATION_CONTROLLER\x10\x02\x12\x12\n\x0eKCM_CONTROLLER\x10\x03\x12\x11\n\rSELF_DESTRUCT\x10\x04*\x8f\x01\n\x1b\x44\x65leteEnterpriseUsersResult\x12\x0b\n\x07SUCCESS\x10\x00\x12\x1a\n\x16NOT_AN_ENTERPRISE_USER\x10\x01\x12\x16\n\x12\x43\x41NNOT_DELETE_SELF\x10\x02\x12$\n BRIDGE_CANNOT_DELETE_ACTIVE_USER\x10\x03\x12\t\n\x05\x45RROR\x10\x04*\x87\x01\n\x15\x43learSecurityDataType\x12\x1e\n\x1aRECALCULATE_SUMMARY_REPORT\x10\x00\x12\'\n#FORCE_CLIENT_CHECK_FOR_MISSING_DATA\x10\x01\x12%\n!FORCE_CLIENT_RESEND_SECURITY_DATA\x10\x02\x42&\n\x18\x63om.keepersecurity.protoB\nEnterpriseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x65nterprise.proto\x12\nEnterprise\"\x84\x01\n\x18\x45nterpriseKeyPairRequest\x12\x1b\n\x13\x65nterprisePublicKey\x18\x01 \x01(\x0c\x12%\n\x1d\x65ncryptedEnterprisePrivateKey\x18\x02 \x01(\x0c\x12$\n\x07keyType\x18\x03 \x01(\x0e\x32\x13.Enterprise.KeyType\"\'\n\x14GetTeamMemberRequest\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\"}\n\x0e\x45nterpriseUser\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x1a\n\x12\x65nterpriseUsername\x18\x03 \x01(\t\x12\x14\n\x0cisShareAdmin\x18\x04 \x01(\x08\x12\x10\n\x08username\x18\x05 \x01(\t\"K\n\x15GetTeamMemberResponse\x12\x32\n\x0e\x65nterpriseUser\x18\x01 \x03(\x0b\x32\x1a.Enterprise.EnterpriseUser\"-\n\x11\x45nterpriseUserIds\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x03(\x03\"B\n\x19\x45nterprisePersonalAccount\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x16\n\x0eOBSOLETE_FIELD\x18\x02 \x01(\x0c\"S\n\x17\x45ncryptedTeamKeyRequest\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTeamKey\x18\x02 \x01(\x0c\x12\r\n\x05\x66orce\x18\x03 \x01(\x08\"+\n\x0fReEncryptedData\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"?\n\x12ReEncryptedRoleKey\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x18\n\x10\x65ncryptedRoleKey\x18\x02 \x01(\x0c\"P\n\x16ReEncryptedUserDataKey\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14userEncryptedDataKey\x18\x02 \x01(\x0c\"\xd8\x02\n\x1bNodeToManagedCompanyRequest\x12\x11\n\tcompanyId\x18\x01 \x01(\x05\x12*\n\x05nodes\x18\x02 \x03(\x0b\x32\x1b.Enterprise.ReEncryptedData\x12*\n\x05roles\x18\x03 \x03(\x0b\x32\x1b.Enterprise.ReEncryptedData\x12*\n\x05users\x18\x04 \x03(\x0b\x32\x1b.Enterprise.ReEncryptedData\x12\x30\n\x08roleKeys\x18\x05 \x03(\x0b\x32\x1e.Enterprise.ReEncryptedRoleKey\x12\x35\n\x08teamKeys\x18\x06 \x03(\x0b\x32#.Enterprise.EncryptedTeamKeyRequest\x12\x39\n\rusersDataKeys\x18\x07 \x03(\x0b\x32\".Enterprise.ReEncryptedUserDataKey\",\n\x08RoleTeam\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x0f\n\x07teamUid\x18\x02 \x01(\x0c\"4\n\tRoleTeams\x12\'\n\trole_team\x18\x01 \x03(\x0b\x32\x14.Enterprise.RoleTeam\"R\n\x0fRoleUserAddKeys\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0f\n\x07treeKey\x18\x02 \x01(\t\x12\x14\n\x0croleAdminKey\x18\x03 \x01(\t\"T\n\x0bRoleUserAdd\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x34\n\x0froleUserAddKeys\x18\x02 \x03(\x0b\x32\x1b.Enterprise.RoleUserAddKeys\"D\n\x13RoleUsersAddRequest\x12-\n\x0croleUserAdds\x18\x01 \x03(\x0b\x32\x17.Enterprise.RoleUserAdd\"\x80\x01\n\x11RoleUserAddResult\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x30\n\x06status\x18\x03 \x01(\x0e\x32 .Enterprise.RoleUserModifyStatus\x12\x0f\n\x07message\x18\x04 \x01(\t\"F\n\x14RoleUsersAddResponse\x12.\n\x07results\x18\x01 \x03(\x0b\x32\x1d.Enterprise.RoleUserAddResult\"<\n\x0eRoleUserRemove\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\"M\n\x16RoleUsersRemoveRequest\x12\x33\n\x0froleUserRemoves\x18\x01 \x03(\x0b\x32\x1a.Enterprise.RoleUserRemove\"\x83\x01\n\x14RoleUserRemoveResult\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x30\n\x06status\x18\x03 \x01(\x0e\x32 .Enterprise.RoleUserModifyStatus\x12\x0f\n\x07message\x18\x04 \x01(\t\"L\n\x17RoleUsersRemoveResponse\x12\x31\n\x07results\x18\x01 \x03(\x0b\x32 .Enterprise.RoleUserRemoveResult\"\xa0\x04\n\x16\x45nterpriseRegistration\x12\x18\n\x10\x65ncryptedTreeKey\x18\x01 \x01(\x0c\x12\x16\n\x0e\x65nterpriseName\x18\x02 \x01(\t\x12\x14\n\x0crootNodeData\x18\x03 \x01(\x0c\x12\x15\n\radminUserData\x18\x04 \x01(\x0c\x12\x11\n\tadminName\x18\x05 \x01(\t\x12\x10\n\x08roleData\x18\x06 \x01(\x0c\x12\x38\n\nrsaKeyPair\x18\x07 \x01(\x0b\x32$.Enterprise.EnterpriseKeyPairRequest\x12\x13\n\x0bnumberSeats\x18\x08 \x01(\x05\x12\x32\n\x0e\x65nterpriseType\x18\t \x01(\x0e\x32\x1a.Enterprise.EnterpriseType\x12\x15\n\rrolePublicKey\x18\n \x01(\x0c\x12*\n\"rolePrivateKeyEncryptedWithRoleKey\x18\x0b \x01(\x0c\x12#\n\x1broleKeyEncryptedWithTreeKey\x18\x0c \x01(\x0c\x12\x38\n\neccKeyPair\x18\r \x01(\x0b\x32$.Enterprise.EnterpriseKeyPairRequest\x12\x18\n\x10\x61llUsersRoleData\x18\x0e \x01(\x0c\x12)\n!roleKeyEncryptedWithUserPublicKey\x18\x0f \x01(\x0c\x12\x18\n\x10\x61pproverRoleData\x18\x10 \x01(\x0c\"H\n\x1a\x44omainPasswordRulesRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x18\n\x10verificationCode\x18\x02 \x01(\t\"\\\n\x19\x44omainPasswordRulesFields\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07minimum\x18\x02 \x01(\x05\x12\x0f\n\x07maximum\x18\x03 \x01(\x05\x12\x0f\n\x07\x61llowed\x18\x04 \x01(\x08\"E\n\x10LoginToMcRequest\x12\x16\n\x0emcEnterpriseId\x18\x01 \x01(\x05\x12\x19\n\x11messageSessionUid\x18\x02 \x01(\x0c\"L\n\x11LoginToMcResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTreeKey\x18\x02 \x01(\t\"g\n\x1b\x44omainPasswordRulesResponse\x12H\n\x19\x64omainPasswordRulesFields\x18\x01 \x03(\x0b\x32%.Enterprise.DomainPasswordRulesFields\"\x88\x01\n\x18\x41pproveUserDeviceRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x01(\x0c\x12\x1e\n\x16\x65ncryptedDeviceDataKey\x18\x03 \x01(\x0c\x12\x14\n\x0c\x64\x65nyApproval\x18\x04 \x01(\x08\"t\n\x19\x41pproveUserDeviceResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x01(\x0c\x12\x0e\n\x06\x66\x61iled\x18\x03 \x01(\x08\x12\x0f\n\x07message\x18\x04 \x01(\t\"Y\n\x19\x41pproveUserDevicesRequest\x12<\n\x0e\x64\x65viceRequests\x18\x01 \x03(\x0b\x32$.Enterprise.ApproveUserDeviceRequest\"\\\n\x1a\x41pproveUserDevicesResponse\x12>\n\x0f\x64\x65viceResponses\x18\x01 \x03(\x0b\x32%.Enterprise.ApproveUserDeviceResponse\"\x87\x01\n\x15\x45nterpriseUserDataKey\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14userEncryptedDataKey\x18\x02 \x01(\x0c\x12\x11\n\tkeyTypeId\x18\x03 \x01(\x05\x12\x0f\n\x07roleKey\x18\x04 \x01(\x0c\x12\x12\n\nprivateKey\x18\x05 \x01(\x0c\"I\n\x16\x45nterpriseUserDataKeys\x12/\n\x04keys\x18\x01 \x03(\x0b\x32!.Enterprise.EnterpriseUserDataKey\"g\n\x1a\x45nterpriseUserDataKeyLight\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1c\n\x14userEncryptedDataKey\x18\x02 \x01(\x0c\x12\x11\n\tkeyTypeId\x18\x03 \x01(\x05\"d\n\x1c\x45nterpriseUserDataKeysByNode\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x34\n\x04keys\x18\x02 \x03(\x0b\x32&.Enterprise.EnterpriseUserDataKeyLight\"^\n$EnterpriseUserDataKeysByNodeResponse\x12\x36\n\x04keys\x18\x01 \x03(\x0b\x32(.Enterprise.EnterpriseUserDataKeysByNode\"2\n\x15\x45nterpriseDataRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"0\n\x13SpecialProvisioning\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"\x84\x02\n\x11GeneralDataEntity\x12\x16\n\x0e\x65nterpriseName\x18\x01 \x01(\t\x12\x1a\n\x12restrictVisibility\x18\x02 \x01(\x08\x12<\n\x13specialProvisioning\x18\x04 \x01(\x0b\x32\x1f.Enterprise.SpecialProvisioning\x12\x30\n\ruserPrivilege\x18\x07 \x01(\x0b\x32\x19.Enterprise.UserPrivilege\x12\x13\n\x0b\x64istributor\x18\x08 \x01(\x08\x12\x1d\n\x15\x66orbidAccountTransfer\x18\t \x01(\x08\x12\x17\n\x0fshowUserOnboard\x18\n \x01(\x08\"\xfd\x01\n\x04Node\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x10\n\x08parentId\x18\x02 \x01(\x03\x12\x10\n\x08\x62ridgeId\x18\x03 \x01(\x03\x12\x0e\n\x06scimId\x18\x04 \x01(\x03\x12\x11\n\tlicenseId\x18\x05 \x01(\x03\x12\x15\n\rencryptedData\x18\x06 \x01(\t\x12\x12\n\nduoEnabled\x18\x07 \x01(\x08\x12\x12\n\nrsaEnabled\x18\x08 \x01(\x08\x12 \n\x14ssoServiceProviderId\x18\t \x01(\x03\x42\x02\x18\x01\x12\x1a\n\x12restrictVisibility\x18\n \x01(\x08\x12!\n\x15ssoServiceProviderIds\x18\x0b \x03(\x03\x42\x02\x10\x01\"\x8e\x01\n\x04Role\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\t\x12\x0f\n\x07keyType\x18\x04 \x01(\t\x12\x14\n\x0cvisibleBelow\x18\x05 \x01(\x08\x12\x16\n\x0enewUserInherit\x18\x06 \x01(\x08\x12\x10\n\x08roleType\x18\x07 \x01(\t\"\xb8\x02\n\x04User\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\t\x12\x0f\n\x07keyType\x18\x04 \x01(\t\x12\x10\n\x08username\x18\x05 \x01(\t\x12\x0e\n\x06status\x18\x06 \x01(\t\x12\x0c\n\x04lock\x18\x07 \x01(\x05\x12\x0e\n\x06userId\x18\x08 \x01(\x05\x12\x1e\n\x16\x61\x63\x63ountShareExpiration\x18\t \x01(\x03\x12\x10\n\x08\x66ullName\x18\n \x01(\t\x12\x10\n\x08jobTitle\x18\x0b \x01(\t\x12\x12\n\ntfaEnabled\x18\x0c \x01(\x08\x12\x46\n\x18transferAcceptanceStatus\x18\r \x01(\x0e\x32$.Enterprise.TransferAcceptanceStatus\"7\n\tUserAlias\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\"\xac\x01\n\x18\x43omplianceReportMetaData\x12\x11\n\treportUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x12\n\nreportName\x18\x03 \x01(\t\x12\x15\n\rdateGenerated\x18\x04 \x01(\x03\x12\x11\n\trunByName\x18\x05 \x01(\t\x12\x16\n\x0enumberOfOwners\x18\x07 \x01(\x05\x12\x17\n\x0fnumberOfRecords\x18\x08 \x01(\x05\"S\n\x0bManagedNode\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x15\n\rmanagedNodeId\x18\x02 \x01(\x03\x12\x1d\n\x15\x63\x61scadeNodeManagement\x18\x03 \x01(\x08\"T\n\x0fUserManagedNode\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x1d\n\x15\x63\x61scadeNodeManagement\x18\x02 \x01(\x08\x12\x12\n\nprivileges\x18\x03 \x03(\t\"w\n\rUserPrivilege\x12\x35\n\x10userManagedNodes\x18\x01 \x03(\x0b\x32\x1b.Enterprise.UserManagedNode\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\t\"4\n\x08RoleUser\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"M\n\rRolePrivilege\x12\x15\n\rmanagedNodeId\x18\x01 \x01(\x03\x12\x0e\n\x06roleId\x18\x02 \x01(\x03\x12\x15\n\rprivilegeType\x18\x03 \x01(\t\"I\n\x0fRoleEnforcement\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x17\n\x0f\x65nforcementType\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\"\xa9\x01\n\x04Team\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x14\n\x0crestrictEdit\x18\x04 \x01(\x08\x12\x15\n\rrestrictShare\x18\x05 \x01(\x08\x12\x14\n\x0crestrictView\x18\x06 \x01(\x08\x12\x15\n\rencryptedData\x18\x07 \x01(\t\x12\x18\n\x10\x65ncryptedTeamKey\x18\x08 \x01(\t\"G\n\x08TeamUser\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x10\n\x08userType\x18\x03 \x01(\t\"K\n\x1aGetDistributorInfoResponse\x12-\n\x0c\x64istributors\x18\x01 \x03(\x0b\x32\x17.Enterprise.Distributor\"B\n\x0b\x44istributor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12%\n\x08mspInfos\x18\x02 \x03(\x0b\x32\x13.Enterprise.MspInfo\"\x9d\x02\n\x07MspInfo\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x16\n\x0e\x65nterpriseName\x18\x02 \x01(\t\x12\x19\n\x11\x61llocatedLicenses\x18\x03 \x01(\x05\x12\x19\n\x11\x61llowedMcProducts\x18\x04 \x03(\t\x12\x15\n\rallowedAddOns\x18\x05 \x03(\t\x12\x17\n\x0fmaxFilePlanType\x18\x06 \x01(\t\x12\x34\n\x10managedCompanies\x18\x07 \x03(\x0b\x32\x1a.Enterprise.ManagedCompany\x12\x1e\n\x16\x61llowUnlimitedLicenses\x18\x08 \x01(\x08\x12(\n\x06\x61\x64\x64Ons\x18\t \x03(\x0b\x32\x18.Enterprise.LicenseAddOn\"\x91\x02\n\x0eManagedCompany\x12\x16\n\x0emcEnterpriseId\x18\x01 \x01(\x05\x12\x18\n\x10mcEnterpriseName\x18\x02 \x01(\t\x12\x11\n\tmspNodeId\x18\x03 \x01(\x03\x12\x15\n\rnumberOfSeats\x18\x04 \x01(\x05\x12\x15\n\rnumberOfUsers\x18\x05 \x01(\x05\x12\x11\n\tproductId\x18\x06 \x01(\t\x12\x11\n\tisExpired\x18\x07 \x01(\x08\x12\x0f\n\x07treeKey\x18\x08 \x01(\t\x12\x15\n\rtree_key_role\x18\t \x01(\x03\x12\x14\n\x0c\x66ilePlanType\x18\n \x01(\t\x12(\n\x06\x61\x64\x64Ons\x18\x0b \x03(\x0b\x32\x18.Enterprise.LicenseAddOn\"R\n\x07MSPPool\x12\x11\n\tproductId\x18\x01 \x01(\t\x12\r\n\x05seats\x18\x02 \x01(\x05\x12\x16\n\x0e\x61vailableSeats\x18\x03 \x01(\x05\x12\r\n\x05stash\x18\x04 \x01(\x05\":\n\nMSPContact\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x16\n\x0e\x65nterpriseName\x18\x02 \x01(\t\"\xec\x01\n\x0cLicenseAddOn\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x02 \x01(\x08\x12\x0f\n\x07isTrial\x18\x03 \x01(\x08\x12\x12\n\nexpiration\x18\x04 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\r\n\x05seats\x18\x06 \x01(\x05\x12\x16\n\x0e\x61\x63tivationTime\x18\x07 \x01(\x03\x12\x19\n\x11includedInProduct\x18\x08 \x01(\x08\x12\x14\n\x0c\x61piCallCount\x18\t \x01(\x05\x12\x17\n\x0ftierDescription\x18\n \x01(\t\x12\x16\n\x0eseatsAllocated\x18\x0b \x01(\x05\"s\n\tMCDefault\x12\x11\n\tmcProduct\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x64\x64Ons\x18\x02 \x03(\t\x12\x14\n\x0c\x66ilePlanType\x18\x03 \x01(\t\x12\x13\n\x0bmaxLicenses\x18\x04 \x01(\x05\x12\x18\n\x10\x66ixedMaxLicenses\x18\x05 \x01(\x08\"\xd2\x01\n\nMSPPermits\x12\x12\n\nrestricted\x18\x01 \x01(\x08\x12\x1a\n\x12maxAllowedLicenses\x18\x02 \x01(\x05\x12\x19\n\x11\x61llowedMcProducts\x18\x03 \x03(\t\x12\x15\n\rallowedAddOns\x18\x04 \x03(\t\x12\x17\n\x0fmaxFilePlanType\x18\x05 \x01(\t\x12\x1e\n\x16\x61llowUnlimitedLicenses\x18\x06 \x01(\x08\x12)\n\nmcDefaults\x18\x07 \x03(\x0b\x32\x15.Enterprise.MCDefault\"\xa0\x04\n\x07License\x12\x0c\n\x04paid\x18\x01 \x01(\x08\x12\x15\n\rnumberOfSeats\x18\x02 \x01(\x05\x12\x12\n\nexpiration\x18\x03 \x01(\x03\x12\x14\n\x0clicenseKeyId\x18\x04 \x01(\x05\x12\x15\n\rproductTypeId\x18\x05 \x01(\x05\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x1b\n\x13\x65nterpriseLicenseId\x18\x07 \x01(\x03\x12\x16\n\x0eseatsAllocated\x18\x08 \x01(\x05\x12\x14\n\x0cseatsPending\x18\t \x01(\x05\x12\x0c\n\x04tier\x18\n \x01(\x05\x12\x16\n\x0e\x66ilePlanTypeId\x18\x0b \x01(\x05\x12\x10\n\x08maxBytes\x18\x0c \x01(\x03\x12\x19\n\x11storageExpiration\x18\r \x01(\x03\x12\x15\n\rlicenseStatus\x18\x0e \x01(\t\x12$\n\x07mspPool\x18\x0f \x03(\x0b\x32\x13.Enterprise.MSPPool\x12)\n\tmanagedBy\x18\x10 \x01(\x0b\x32\x16.Enterprise.MSPContact\x12(\n\x06\x61\x64\x64Ons\x18\x11 \x03(\x0b\x32\x18.Enterprise.LicenseAddOn\x12\x17\n\x0fnextBillingDate\x18\x12 \x01(\x03\x12\x17\n\x0fhasMSPLegacyLog\x18\x13 \x01(\x08\x12*\n\nmspPermits\x18\x14 \x01(\x0b\x32\x16.Enterprise.MSPPermits\x12\x13\n\x0b\x64istributor\x18\x15 \x01(\x08\"n\n\x06\x42ridge\x12\x10\n\x08\x62ridgeId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x18\n\x10wanIpEnforcement\x18\x03 \x01(\t\x12\x18\n\x10lanIpEnforcement\x18\x04 \x01(\t\x12\x0e\n\x06status\x18\x05 \x01(\t\"t\n\x04Scim\x12\x0e\n\x06scimId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\nlastSynced\x18\x04 \x01(\x03\x12\x12\n\nrolePrefix\x18\x05 \x01(\t\x12\x14\n\x0cuniqueGroups\x18\x06 \x01(\x08\"L\n\x0e\x45mailProvision\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0e\n\x06\x64omain\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\"R\n\nQueuedTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x15\n\rencryptedData\x18\x04 \x01(\t\"0\n\x0eQueuedTeamUser\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\r\n\x05users\x18\x02 \x03(\x03\"\xa4\x01\n\x0eTeamsAddResult\x12\x34\n\x11successfulTeamAdd\x18\x01 \x03(\x0b\x32\x19.Enterprise.TeamAddResult\x12\x36\n\x13unsuccessfulTeamAdd\x18\x02 \x03(\x0b\x32\x19.Enterprise.TeamAddResult\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\x14\n\x0c\x65rrorMessage\x18\x04 \x01(\t\"U\n\rTeamAddResult\x12\x1e\n\x04team\x18\x01 \x01(\x0b\x32\x10.Enterprise.Team\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x01(\t\"\x91\x01\n\nSsoService\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06sp_url\x18\x04 \x01(\t\x12\x16\n\x0einviteNewUsers\x18\x05 \x01(\x08\x12\x0e\n\x06\x61\x63tive\x18\x06 \x01(\x08\x12\x0f\n\x07isCloud\x18\x07 \x01(\x08\"1\n\x10ReportFilterUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\r\n\x05\x65mail\x18\x02 \x01(\t\"\x97\x02\n\x1d\x44\x65viceRequestForAdminApproval\x12\x10\n\x08\x64\x65viceId\x18\x01 \x01(\x03\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x03 \x01(\x0c\x12\x17\n\x0f\x64\x65vicePublicKey\x18\x04 \x01(\x0c\x12\x12\n\ndeviceName\x18\x05 \x01(\t\x12\x15\n\rclientVersion\x18\x06 \x01(\t\x12\x12\n\ndeviceType\x18\x07 \x01(\t\x12\x0c\n\x04\x64\x61te\x18\x08 \x01(\x03\x12\x11\n\tipAddress\x18\t \x01(\t\x12\x10\n\x08location\x18\n \x01(\t\x12\r\n\x05\x65mail\x18\x0b \x01(\t\x12\x12\n\naccountUid\x18\x0c \x01(\x0c\"`\n\x0e\x45nterpriseData\x12\x30\n\x06\x65ntity\x18\x01 \x01(\x0e\x32 .Enterprise.EnterpriseDataEntity\x12\x0e\n\x06\x64\x65lete\x18\x02 \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x03 \x03(\x0c\"\xd0\x01\n\x16\x45nterpriseDataResponse\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\x12,\n\x0b\x63\x61\x63heStatus\x18\x03 \x01(\x0e\x32\x17.Enterprise.CacheStatus\x12(\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x1a.Enterprise.EnterpriseData\x12\x32\n\x0bgeneralData\x18\x05 \x01(\x0b\x32\x1d.Enterprise.GeneralDataEntity\"*\n\rBackupRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"\x98\x01\n\x0c\x42\x61\x63kupRecord\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12*\n\x07keyType\x18\x04 \x01(\x0e\x32\x19.Enterprise.BackupKeyType\x12\x0f\n\x07version\x18\x05 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\r\n\x05\x65xtra\x18\x07 \x01(\x0c\".\n\tBackupKey\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\tbackupKey\x18\x02 \x01(\x0c\"\x8d\x02\n\nBackupUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x10\n\x08userName\x18\x02 \x01(\t\x12\x0f\n\x07\x64\x61taKey\x18\x03 \x01(\x0c\x12\x36\n\x0b\x64\x61taKeyType\x18\x04 \x01(\x0e\x32!.Enterprise.BackupUserDataKeyType\x12\x12\n\nprivateKey\x18\x05 \x01(\x0c\x12\x0f\n\x07treeKey\x18\x06 \x01(\x0c\x12.\n\x0btreeKeyType\x18\x07 \x01(\x0e\x32\x19.Enterprise.BackupKeyType\x12)\n\nbackupKeys\x18\x08 \x03(\x0b\x32\x15.Enterprise.BackupKey\x12\x14\n\x0cprivateECKey\x18\t \x01(\x0c\"\x9e\x01\n\x0e\x42\x61\x63kupResponse\x12\x1f\n\x17\x65nterpriseEccPrivateKey\x18\x01 \x01(\x0c\x12%\n\x05users\x18\x02 \x03(\x0b\x32\x16.Enterprise.BackupUser\x12)\n\x07records\x18\x03 \x03(\x0b\x32\x18.Enterprise.BackupRecord\x12\x19\n\x11\x63ontinuationToken\x18\x04 \x01(\x0c\"e\n\nBackupFile\x12\x0c\n\x04user\x18\x01 \x01(\t\x12\x11\n\tbackupUid\x18\x02 \x01(\x0c\x12\x10\n\x08\x66ileName\x18\x03 \x01(\t\x12\x0f\n\x07\x63reated\x18\x04 \x01(\x03\x12\x13\n\x0b\x64ownloadUrl\x18\x05 \x01(\t\"8\n\x0f\x42\x61\x63kupsResponse\x12%\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x16.Enterprise.BackupFile\".\n\x1cGetEnterpriseDataKeysRequest\x12\x0e\n\x06roleId\x18\x01 \x03(\x03\"\xff\x01\n\x1dGetEnterpriseDataKeysResponse\x12:\n\x12reEncryptedRoleKey\x18\x01 \x03(\x0b\x32\x1e.Enterprise.ReEncryptedRoleKey\x12$\n\x07roleKey\x18\x02 \x03(\x0b\x32\x13.Enterprise.RoleKey\x12\"\n\x06mspKey\x18\x03 \x01(\x0b\x32\x12.Enterprise.MspKey\x12\x32\n\x0e\x65nterpriseKeys\x18\x04 \x01(\x0b\x32\x1a.Enterprise.EnterpriseKeys\x12$\n\x07treeKey\x18\x05 \x01(\x0b\x32\x13.Enterprise.TreeKey\"^\n\x07RoleKey\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x14\n\x0c\x65ncryptedKey\x18\x02 \x01(\t\x12-\n\x07keyType\x18\x03 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"d\n\x06MspKey\x12\x1b\n\x13\x65ncryptedMspTreeKey\x18\x01 \x01(\t\x12=\n\x17\x65ncryptedMspTreeKeyType\x18\x02 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"|\n\x0e\x45nterpriseKeys\x12\x14\n\x0crsaPublicKey\x18\x01 \x01(\x0c\x12\x1e\n\x16rsaEncryptedPrivateKey\x18\x02 \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\x03 \x01(\x0c\x12\x1e\n\x16\x65\x63\x63\x45ncryptedPrivateKey\x18\x04 \x01(\x0c\"H\n\x07TreeKey\x12\x0f\n\x07treeKey\x18\x01 \x01(\t\x12,\n\tkeyTypeId\x18\x02 \x01(\x0e\x32\x19.Enterprise.BackupKeyType\"E\n\x14SharedRecordResponse\x12-\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1d.Enterprise.SharedRecordEvent\"p\n\x11SharedRecordEvent\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08userName\x18\x02 \x01(\t\x12\x0f\n\x07\x63\x61nEdit\x18\x03 \x01(\x08\x12\x12\n\ncanReshare\x18\x04 \x01(\x08\x12\x11\n\tshareFrom\x18\x05 \x01(\x05\".\n\x1cSetRestrictVisibilityRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\"\xd0\x01\n\x0eUserAddRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12-\n\x07keyType\x18\x04 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x05 \x01(\t\x12\x10\n\x08jobTitle\x18\x06 \x01(\t\x12\r\n\x05\x65mail\x18\x07 \x01(\t\x12\x1b\n\x13suppressEmailInvite\x18\x08 \x01(\x08\":\n\x11UserUpdateRequest\x12%\n\x05users\x18\x01 \x03(\x0b\x32\x16.Enterprise.UserUpdate\"\xaf\x01\n\nUserUpdate\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12-\n\x07keyType\x18\x04 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x05 \x01(\t\x12\x10\n\x08jobTitle\x18\x06 \x01(\t\x12\r\n\x05\x65mail\x18\x07 \x01(\t\"A\n\x12UserUpdateResponse\x12+\n\x05users\x18\x01 \x03(\x0b\x32\x1c.Enterprise.UserUpdateResult\"Z\n\x10UserUpdateResult\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12,\n\x06status\x18\x02 \x01(\x0e\x32\x1c.Enterprise.UserUpdateStatus\"J\n\x1d\x43omplianceRecordOwnersRequest\x12\x0f\n\x07nodeIds\x18\x01 \x03(\x03\x12\x18\n\x10includeNonShared\x18\x02 \x01(\x08\"O\n\x1e\x43omplianceRecordOwnersResponse\x12-\n\x0crecordOwners\x18\x01 \x03(\x0b\x32\x17.Enterprise.RecordOwner\"7\n\x0bRecordOwner\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0e\n\x06shared\x18\x02 \x01(\x08\"\xa6\x01\n PreliminaryComplianceDataRequest\x12\x19\n\x11\x65nterpriseUserIds\x18\x01 \x03(\x03\x12\x18\n\x10includeNonShared\x18\x02 \x01(\x08\x12\x19\n\x11\x63ontinuationToken\x18\x03 \x01(\x0c\x12\x32\n*includeTotalMatchingRecordsInFirstResponse\x18\x04 \x01(\x08\"\x9f\x01\n!PreliminaryComplianceDataResponse\x12\x30\n\rauditUserData\x18\x01 \x03(\x0b\x32\x19.Enterprise.AuditUserData\x12\x19\n\x11\x63ontinuationToken\x18\x02 \x01(\x0c\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\x12\x1c\n\x14totalMatchingRecords\x18\x04 \x01(\x05\"K\n\x0f\x41uditUserRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x15\n\rencryptedData\x18\x02 \x01(\x0c\x12\x0e\n\x06shared\x18\x03 \x01(\x08\"\x8d\x01\n\rAuditUserData\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x35\n\x10\x61uditUserRecords\x18\x02 \x03(\x0b\x32\x1b.Enterprise.AuditUserRecord\x12+\n\x06status\x18\x03 \x01(\x0e\x32\x1b.Enterprise.AuditUserStatus\"\x7f\n\x17\x43omplianceReportFilters\x12\x14\n\x0crecordTitles\x18\x01 \x03(\t\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\x12\x11\n\tjobTitles\x18\x03 \x03(\x03\x12\x0c\n\x04urls\x18\x04 \x03(\t\x12\x19\n\x11\x65nterpriseUserIds\x18\x05 \x03(\x03\"\x7f\n\x17\x43omplianceReportRequest\x12<\n\x13\x63omplianceReportRun\x18\x01 \x01(\x0b\x32\x1f.Enterprise.ComplianceReportRun\x12\x12\n\nreportName\x18\x02 \x01(\t\x12\x12\n\nsaveReport\x18\x03 \x01(\x08\"\x85\x01\n\x13\x43omplianceReportRun\x12N\n\x17reportCriteriaAndFilter\x18\x01 \x01(\x0b\x32-.Enterprise.ComplianceReportCriteriaAndFilter\x12\r\n\x05users\x18\x02 \x03(\x03\x12\x0f\n\x07records\x18\x03 \x03(\x0c\"\xfc\x01\n!ComplianceReportCriteriaAndFilter\x12\x0e\n\x06nodeId\x18\x01 \x01(\x03\x12\x13\n\x0b\x63riteriaUid\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63riteriaName\x18\x03 \x01(\t\x12\x36\n\x08\x63riteria\x18\x04 \x01(\x0b\x32$.Enterprise.ComplianceReportCriteria\x12\x33\n\x07\x66ilters\x18\x05 \x03(\x0b\x32\".Enterprise.ComplianceReportFilter\x12\x14\n\x0clastModified\x18\x06 \x01(\x03\x12\x19\n\x11nodeEncryptedData\x18\x07 \x01(\x0c\"b\n\x18\x43omplianceReportCriteria\x12\x11\n\tjobTitles\x18\x01 \x03(\t\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\x12\x18\n\x10includeNonShared\x18\x03 \x01(\x08\"x\n\x16\x43omplianceReportFilter\x12\x14\n\x0crecordTitles\x18\x01 \x03(\t\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\x12\x11\n\tjobTitles\x18\x03 \x03(\t\x12\x0c\n\x04urls\x18\x04 \x03(\t\x12\x13\n\x0brecordTypes\x18\x05 \x03(\t\"\xa1\x05\n\x18\x43omplianceReportResponse\x12\x15\n\rdateGenerated\x18\x01 \x01(\x03\x12\x15\n\rrunByUserName\x18\x02 \x01(\t\x12\x12\n\nreportName\x18\x03 \x01(\t\x12\x11\n\treportUid\x18\x04 \x01(\x0c\x12<\n\x13\x63omplianceReportRun\x18\x05 \x01(\x0b\x32\x1f.Enterprise.ComplianceReportRun\x12-\n\x0cuserProfiles\x18\x06 \x03(\x0b\x32\x17.Enterprise.UserProfile\x12)\n\nauditTeams\x18\x07 \x03(\x0b\x32\x15.Enterprise.AuditTeam\x12-\n\x0c\x61uditRecords\x18\x08 \x03(\x0b\x32\x17.Enterprise.AuditRecord\x12+\n\x0buserRecords\x18\t \x03(\x0b\x32\x16.Enterprise.UserRecord\x12;\n\x13sharedFolderRecords\x18\n \x03(\x0b\x32\x1e.Enterprise.SharedFolderRecord\x12\x37\n\x11sharedFolderUsers\x18\x0b \x03(\x0b\x32\x1c.Enterprise.SharedFolderUser\x12\x37\n\x11sharedFolderTeams\x18\x0c \x03(\x0b\x32\x1c.Enterprise.SharedFolderTeam\x12\x31\n\x0e\x61uditTeamUsers\x18\r \x03(\x0b\x32\x19.Enterprise.AuditTeamUser\x12)\n\nauditRoles\x18\x0e \x03(\x0b\x32\x15.Enterprise.AuditRole\x12/\n\rlinkedRecords\x18\x0f \x03(\x0b\x32\x18.Enterprise.LinkedRecord\"\x81\x01\n\x0b\x41uditRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x11\n\tauditData\x18\x02 \x01(\x0c\x12\x16\n\x0ehasAttachments\x18\x03 \x01(\x08\x12\x0f\n\x07inTrash\x18\x04 \x01(\x08\x12\x10\n\x08treeLeft\x18\x05 \x01(\x05\x12\x11\n\ttreeRight\x18\x06 \x01(\x05\"\x80\x02\n\tAuditRole\x12\x0e\n\x06roleId\x18\x01 \x01(\x03\x12\x15\n\rencryptedData\x18\x02 \x01(\x0c\x12&\n\x1erestrictShareOutsideEnterprise\x18\x03 \x01(\x08\x12\x18\n\x10restrictShareAll\x18\x04 \x01(\x08\x12\"\n\x1arestrictShareOfAttachments\x18\x05 \x01(\x08\x12)\n!restrictMaskPasswordsWhileEditing\x18\x06 \x01(\x08\x12;\n\x13roleNodeManagements\x18\x07 \x03(\x0b\x32\x1e.Enterprise.RoleNodeManagement\"^\n\x12RoleNodeManagement\x12\x10\n\x08treeLeft\x18\x01 \x01(\x05\x12\x11\n\ttreeRight\x18\x02 \x01(\x05\x12\x0f\n\x07\x63\x61scade\x18\x03 \x01(\x08\x12\x12\n\nprivileges\x18\x04 \x01(\x05\"k\n\x0bUserProfile\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08\x66ullName\x18\x02 \x01(\t\x12\x10\n\x08jobTitle\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x0f\n\x07roleIds\x18\x05 \x03(\x03\"=\n\x10RecordPermission\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x16\n\x0epermissionBits\x18\x02 \x01(\x05\"_\n\nUserRecord\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x37\n\x11recordPermissions\x18\x02 \x03(\x0b\x32\x1c.Enterprise.RecordPermission\"[\n\tAuditTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x10\n\x08teamName\x18\x02 \x01(\t\x12\x14\n\x0crestrictEdit\x18\x03 \x01(\x08\x12\x15\n\rrestrictShare\x18\x04 \x01(\x08\";\n\rAuditTeamUser\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\"\x9f\x01\n\x12SharedFolderRecord\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x37\n\x11recordPermissions\x18\x02 \x03(\x0b\x32\x1c.Enterprise.RecordPermission\x12\x37\n\x11shareAdminRecords\x18\x03 \x03(\x0b\x32\x1c.Enterprise.ShareAdminRecord\"M\n\x10ShareAdminRecord\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x1f\n\x17recordPermissionIndexes\x18\x02 \x03(\x05\"F\n\x10SharedFolderUser\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x19\n\x11\x65nterpriseUserIds\x18\x02 \x03(\x03\"=\n\x10SharedFolderTeam\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08teamUids\x18\x02 \x03(\x0c\"/\n\x1aGetComplianceReportRequest\x12\x11\n\treportUid\x18\x01 \x01(\x0c\"2\n\x1bGetComplianceReportResponse\x12\x13\n\x0b\x64ownloadUrl\x18\x01 \x01(\t\"6\n\x1f\x43omplianceReportCriteriaRequest\x12\x13\n\x0b\x63riteriaUid\x18\x01 \x01(\x0c\";\n$SaveComplianceReportCriteriaResponse\x12\x13\n\x0b\x63riteriaUid\x18\x01 \x01(\x0c\"4\n\x0cLinkedRecord\x12\x10\n\x08ownerUid\x18\x01 \x01(\x0c\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\"W\n\x17GetSharingAdminsRequest\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x10\n\x08username\x18\x03 \x01(\t\"\xe0\x01\n\x0eUserProfileExt\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x10\n\x08\x66ullName\x18\x02 \x01(\t\x12\x10\n\x08jobTitle\x18\x03 \x01(\t\x12\x14\n\x0cisMSPMCAdmin\x18\x04 \x01(\x08\x12\x18\n\x10isInSharedFolder\x18\x05 \x01(\x08\x12&\n\x1eisShareAdminForRequestedObject\x18\x06 \x01(\x08\x12(\n isShareAdminForSharedFolderOwner\x18\x07 \x01(\x08\x12\x19\n\x11hasAccessToObject\x18\x08 \x01(\x08\"O\n\x18GetSharingAdminsResponse\x12\x33\n\x0fuserProfileExts\x18\x01 \x03(\x0b\x32\x1a.Enterprise.UserProfileExt\"_\n\x1eTeamsEnterpriseUsersAddRequest\x12=\n\x05teams\x18\x01 \x03(\x0b\x32..Enterprise.TeamsEnterpriseUsersAddTeamRequest\"t\n\"TeamsEnterpriseUsersAddTeamRequest\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12=\n\x05users\x18\x02 \x03(\x0b\x32..Enterprise.TeamsEnterpriseUsersAddUserRequest\"\xab\x01\n\"TeamsEnterpriseUsersAddUserRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12*\n\x08userType\x18\x02 \x01(\x0e\x32\x18.Enterprise.TeamUserType\x12\x13\n\x07teamKey\x18\x03 \x01(\tB\x02\x18\x01\x12*\n\x0ctypedTeamKey\x18\x04 \x01(\x0b\x32\x14.Enterprise.TypedKey\"F\n\x08TypedKey\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12-\n\x07keyType\x18\x02 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\"s\n\x1fTeamsEnterpriseUsersAddResponse\x12>\n\x05teams\x18\x01 \x03(\x0b\x32/.Enterprise.TeamsEnterpriseUsersAddTeamResponse\x12\x10\n\x08revision\x18\x02 \x01(\x03\"\xc4\x01\n#TeamsEnterpriseUsersAddTeamResponse\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12>\n\x05users\x18\x02 \x03(\x0b\x32/.Enterprise.TeamsEnterpriseUsersAddUserResponse\x12\x0f\n\x07success\x18\x03 \x01(\x08\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x12\n\nresultCode\x18\x05 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x06 \x01(\t\"\x9f\x01\n#TeamsEnterpriseUsersAddUserResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0f\n\x07success\x18\x03 \x01(\x08\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x12\n\nresultCode\x18\x05 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x06 \x01(\t\"M\n\x0b\x44omainAlias\x12\x0e\n\x06\x64omain\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x0f\n\x07message\x18\x04 \x01(\t\"B\n\x12\x44omainAliasRequest\x12,\n\x0b\x64omainAlias\x18\x01 \x03(\x0b\x32\x17.Enterprise.DomainAlias\"C\n\x13\x44omainAliasResponse\x12,\n\x0b\x64omainAlias\x18\x01 \x03(\x0b\x32\x17.Enterprise.DomainAlias\"m\n\x1f\x45nterpriseUsersProvisionRequest\x12\x33\n\x05users\x18\x01 \x03(\x0b\x32$.Enterprise.EnterpriseUsersProvision\x12\x15\n\rclientVersion\x18\x02 \x01(\t\"\xb6\x03\n\x18\x45nterpriseUsersProvision\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x15\n\rencryptedData\x18\x04 \x01(\t\x12-\n\x07keyType\x18\x05 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x06 \x01(\t\x12\x10\n\x08jobTitle\x18\x07 \x01(\t\x12\x1e\n\x16\x65nterpriseUsersDataKey\x18\x08 \x01(\x0c\x12\x14\n\x0c\x61uthVerifier\x18\t \x01(\x0c\x12\x18\n\x10\x65ncryptionParams\x18\n \x01(\x0c\x12\x14\n\x0crsaPublicKey\x18\x0b \x01(\x0c\x12\x1e\n\x16rsaEncryptedPrivateKey\x18\x0c \x01(\x0c\x12\x14\n\x0c\x65\x63\x63PublicKey\x18\r \x01(\x0c\x12\x1e\n\x16\x65\x63\x63\x45ncryptedPrivateKey\x18\x0e \x01(\x0c\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x0f \x01(\x0c\x12\x1a\n\x12\x65ncryptedClientKey\x18\x10 \x01(\x0c\"_\n EnterpriseUsersProvisionResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.Enterprise.EnterpriseUsersProvisionResult\"q\n\x1e\x45nterpriseUsersProvisionResult\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x04 \x01(\t\"a\n\x19\x45nterpriseUsersAddRequest\x12-\n\x05users\x18\x01 \x03(\x0b\x32\x1e.Enterprise.EnterpriseUsersAdd\x12\x15\n\rclientVersion\x18\x02 \x01(\t\"\x8c\x02\n\x12\x45nterpriseUsersAdd\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x03\x12\x15\n\rencryptedData\x18\x04 \x01(\t\x12-\n\x07keyType\x18\x05 \x01(\x0e\x32\x1c.Enterprise.EncryptedKeyType\x12\x10\n\x08\x66ullName\x18\x06 \x01(\t\x12\x10\n\x08jobTitle\x18\x07 \x01(\t\x12\x1b\n\x13suppressEmailInvite\x18\x08 \x01(\x08\x12\x15\n\rinviteeLocale\x18\t \x01(\t\x12\x0c\n\x04move\x18\n \x01(\x08\x12\x0e\n\x06roleId\x18\x0b \x01(\x03\"\x9b\x01\n\x1a\x45nterpriseUsersAddResponse\x12\x35\n\x07results\x18\x01 \x03(\x0b\x32$.Enterprise.EnterpriseUsersAddResult\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x05 \x01(\t\"\x96\x01\n\x18\x45nterpriseUsersAddResult\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x18\n\x10verificationCode\x18\x03 \x01(\t\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\x12\x0f\n\x07message\x18\x05 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x06 \x01(\t\"\xb9\x01\n\x17UpdateMSPPermitsRequest\x12\x17\n\x0fmspEnterpriseId\x18\x01 \x01(\x05\x12\x1a\n\x12maxAllowedLicenses\x18\x02 \x01(\x05\x12\x19\n\x11\x61llowedMcProducts\x18\x03 \x03(\t\x12\x15\n\rallowedAddOns\x18\x04 \x03(\t\x12\x17\n\x0fmaxFilePlanType\x18\x05 \x01(\t\x12\x1e\n\x16\x61llowUnlimitedLicenses\x18\x06 \x01(\x08\"9\n\x1c\x44\x65leteEnterpriseUsersRequest\x12\x19\n\x11\x65nterpriseUserIds\x18\x01 \x03(\x03\"o\n\x1a\x44\x65leteEnterpriseUserStatus\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x37\n\x06status\x18\x02 \x01(\x0e\x32\'.Enterprise.DeleteEnterpriseUsersResult\"]\n\x1d\x44\x65leteEnterpriseUsersResponse\x12<\n\x0c\x64\x65leteStatus\x18\x01 \x03(\x0b\x32&.Enterprise.DeleteEnterpriseUserStatus\"w\n\x18\x43learSecurityDataRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x03(\x03\x12\x10\n\x08\x61llUsers\x18\x02 \x01(\x08\x12/\n\x04type\x18\x03 \x01(\x0e\x32!.Enterprise.ClearSecurityDataType*\x1b\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x07\n\x03\x45\x43\x43\x10\x01*\xe6\x01\n\x14RoleUserModifyStatus\x12\x0f\n\x0bROLE_EXISTS\x10\x00\x12\x14\n\x10MISSING_TREE_KEY\x10\x01\x12\x14\n\x10MISSING_ROLE_KEY\x10\x02\x12\x1e\n\x1aINVALID_ENTERPRISE_USER_ID\x10\x03\x12\x1b\n\x17PENDING_ENTERPRISE_USER\x10\x04\x12\x13\n\x0fINVALID_NODE_ID\x10\x05\x12!\n\x1dMAY_NOT_REMOVE_SELF_FROM_ROLE\x10\x06\x12\x1c\n\x18MUST_HAVE_ONE_USER_ADMIN\x10\x07*=\n\x0e\x45nterpriseType\x12\x17\n\x13\x45NTERPRISE_STANDARD\x10\x00\x12\x12\n\x0e\x45NTERPRISE_MSP\x10\x01*s\n\x18TransferAcceptanceStatus\x12\r\n\tUNDEFINED\x10\x00\x12\x10\n\x0cNOT_REQUIRED\x10\x01\x12\x10\n\x0cNOT_ACCEPTED\x10\x02\x12\x16\n\x12PARTIALLY_ACCEPTED\x10\x03\x12\x0c\n\x08\x41\x43\x43\x45PTED\x10\x04*\x8a\x04\n\x14\x45nterpriseDataEntity\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05NODES\x10\x01\x12\t\n\x05ROLES\x10\x02\x12\t\n\x05USERS\x10\x03\x12\t\n\x05TEAMS\x10\x04\x12\x0e\n\nTEAM_USERS\x10\x05\x12\x0e\n\nROLE_USERS\x10\x06\x12\x13\n\x0fROLE_PRIVILEGES\x10\x07\x12\x15\n\x11ROLE_ENFORCEMENTS\x10\x08\x12\x0e\n\nROLE_TEAMS\x10\t\x12\x0c\n\x08LICENSES\x10\n\x12\x11\n\rMANAGED_NODES\x10\x0b\x12\x15\n\x11MANAGED_COMPANIES\x10\x0c\x12\x0b\n\x07\x42RIDGES\x10\r\x12\t\n\x05SCIMS\x10\x0e\x12\x13\n\x0f\x45MAIL_PROVISION\x10\x0f\x12\x10\n\x0cQUEUED_TEAMS\x10\x10\x12\x15\n\x11QUEUED_TEAM_USERS\x10\x11\x12\x10\n\x0cSSO_SERVICES\x10\x12\x12\x17\n\x13REPORT_FILTER_USERS\x10\x13\x12&\n\"DEVICES_REQUEST_FOR_ADMIN_APPROVAL\x10\x14\x12\x10\n\x0cUSER_ALIASES\x10\x15\x12)\n%COMPLIANCE_REPORT_CRITERIA_AND_FILTER\x10\x16\x12\x16\n\x12\x43OMPLIANCE_REPORTS\x10\x17\x12\'\n#QUEUED_TEAM_USERS_INCLUDING_PENDING\x10\x18*\"\n\x0b\x43\x61\x63heStatus\x12\x08\n\x04KEEP\x10\x00\x12\t\n\x05\x43LEAR\x10\x01*\x93\x01\n\rBackupKeyType\x12\n\n\x06NO_KEY\x10\x00\x12\x19\n\x15\x45NCRYPTED_BY_DATA_KEY\x10\x01\x12\x1b\n\x17\x45NCRYPTED_BY_PUBLIC_KEY\x10\x02\x12\x1d\n\x19\x45NCRYPTED_BY_DATA_KEY_GCM\x10\x03\x12\x1f\n\x1b\x45NCRYPTED_BY_PUBLIC_KEY_ECC\x10\x04*:\n\x15\x42\x61\x63kupUserDataKeyType\x12\x07\n\x03OWN\x10\x00\x12\x18\n\x14SHARED_TO_ENTERPRISE\x10\x01*\xa5\x01\n\x10\x45ncryptedKeyType\x12\r\n\tKT_NO_KEY\x10\x00\x12\x1c\n\x18KT_ENCRYPTED_BY_DATA_KEY\x10\x01\x12\x1e\n\x1aKT_ENCRYPTED_BY_PUBLIC_KEY\x10\x02\x12 \n\x1cKT_ENCRYPTED_BY_DATA_KEY_GCM\x10\x03\x12\"\n\x1eKT_ENCRYPTED_BY_PUBLIC_KEY_ECC\x10\x04*\x8e\x02\n\x12\x45nterpriseFlagType\x12\x0b\n\x07INVALID\x10\x00\x12\x1a\n\x16\x41LLOW_PERSONAL_LICENSE\x10\x01\x12\x18\n\x14SPECIAL_PROVISIONING\x10\x02\x12\x10\n\x0cRECORD_TYPES\x10\x03\x12\x13\n\x0fSECRETS_MANAGER\x10\x04\x12\x15\n\x11\x45NTERPRISE_LOCKED\x10\x05\x12\x15\n\x11\x46ORBID_KEY_TYPE_2\x10\x06\x12\x15\n\x11\x43ONSOLE_ONBOARDED\x10\x07\x12\x1b\n\x17\x46ORBID_ACCOUNT_TRANSFER\x10\x08\x12\x15\n\x11NPS_POPUP_OPT_OUT\x10\t\x12\x15\n\x11SHOW_USER_ONBOARD\x10\n*E\n\x10UserUpdateStatus\x12\x12\n\x0eUSER_UPDATE_OK\x10\x00\x12\x1d\n\x19USER_UPDATE_ACCESS_DENIED\x10\x01*I\n\x0f\x41uditUserStatus\x12\x06\n\x02OK\x10\x00\x12\x11\n\rACCESS_DENIED\x10\x01\x12\x1b\n\x17NO_LONGER_IN_ENTERPRISE\x10\x02*3\n\x0cTeamUserType\x12\x08\n\x04USER\x10\x00\x12\t\n\x05\x41\x44MIN\x10\x01\x12\x0e\n\nADMIN_ONLY\x10\x02*x\n\rAppClientType\x12\x0c\n\x08NOT_USED\x10\x00\x12\x0b\n\x07GENERAL\x10\x01\x12%\n!DISCOVERY_AND_ROTATION_CONTROLLER\x10\x02\x12\x12\n\x0eKCM_CONTROLLER\x10\x03\x12\x11\n\rSELF_DESTRUCT\x10\x04*\x8f\x01\n\x1b\x44\x65leteEnterpriseUsersResult\x12\x0b\n\x07SUCCESS\x10\x00\x12\x1a\n\x16NOT_AN_ENTERPRISE_USER\x10\x01\x12\x16\n\x12\x43\x41NNOT_DELETE_SELF\x10\x02\x12$\n BRIDGE_CANNOT_DELETE_ACTIVE_USER\x10\x03\x12\t\n\x05\x45RROR\x10\x04*\x87\x01\n\x15\x43learSecurityDataType\x12\x1e\n\x1aRECALCULATE_SUMMARY_REPORT\x10\x00\x12\'\n#FORCE_CLIENT_CHECK_FOR_MISSING_DATA\x10\x01\x12%\n!FORCE_CLIENT_RESEND_SECURITY_DATA\x10\x02\x42&\n\x18\x63om.keepersecurity.protoB\nEnterpriseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -38,38 +30,38 @@ _globals['_NODE'].fields_by_name['ssoServiceProviderIds']._serialized_options = b'\020\001' _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST'].fields_by_name['teamKey']._loaded_options = None _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST'].fields_by_name['teamKey']._serialized_options = b'\030\001' - _globals['_KEYTYPE']._serialized_start=19291 - _globals['_KEYTYPE']._serialized_end=19318 - _globals['_ROLEUSERMODIFYSTATUS']._serialized_start=19321 - _globals['_ROLEUSERMODIFYSTATUS']._serialized_end=19551 - _globals['_ENTERPRISETYPE']._serialized_start=19553 - _globals['_ENTERPRISETYPE']._serialized_end=19614 - _globals['_TRANSFERACCEPTANCESTATUS']._serialized_start=19616 - _globals['_TRANSFERACCEPTANCESTATUS']._serialized_end=19731 - _globals['_ENTERPRISEDATAENTITY']._serialized_start=19734 - _globals['_ENTERPRISEDATAENTITY']._serialized_end=20256 - _globals['_CACHESTATUS']._serialized_start=20258 - _globals['_CACHESTATUS']._serialized_end=20292 - _globals['_BACKUPKEYTYPE']._serialized_start=20295 - _globals['_BACKUPKEYTYPE']._serialized_end=20442 - _globals['_BACKUPUSERDATAKEYTYPE']._serialized_start=20444 - _globals['_BACKUPUSERDATAKEYTYPE']._serialized_end=20502 - _globals['_ENCRYPTEDKEYTYPE']._serialized_start=20505 - _globals['_ENCRYPTEDKEYTYPE']._serialized_end=20670 - _globals['_ENTERPRISEFLAGTYPE']._serialized_start=20673 - _globals['_ENTERPRISEFLAGTYPE']._serialized_end=20943 - _globals['_USERUPDATESTATUS']._serialized_start=20945 - _globals['_USERUPDATESTATUS']._serialized_end=21014 - _globals['_AUDITUSERSTATUS']._serialized_start=21016 - _globals['_AUDITUSERSTATUS']._serialized_end=21089 - _globals['_TEAMUSERTYPE']._serialized_start=21091 - _globals['_TEAMUSERTYPE']._serialized_end=21142 - _globals['_APPCLIENTTYPE']._serialized_start=21144 - _globals['_APPCLIENTTYPE']._serialized_end=21264 - _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_start=21267 - _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_end=21410 - _globals['_CLEARSECURITYDATATYPE']._serialized_start=21413 - _globals['_CLEARSECURITYDATATYPE']._serialized_end=21548 + _globals['_KEYTYPE']._serialized_start=19317 + _globals['_KEYTYPE']._serialized_end=19344 + _globals['_ROLEUSERMODIFYSTATUS']._serialized_start=19347 + _globals['_ROLEUSERMODIFYSTATUS']._serialized_end=19577 + _globals['_ENTERPRISETYPE']._serialized_start=19579 + _globals['_ENTERPRISETYPE']._serialized_end=19640 + _globals['_TRANSFERACCEPTANCESTATUS']._serialized_start=19642 + _globals['_TRANSFERACCEPTANCESTATUS']._serialized_end=19757 + _globals['_ENTERPRISEDATAENTITY']._serialized_start=19760 + _globals['_ENTERPRISEDATAENTITY']._serialized_end=20282 + _globals['_CACHESTATUS']._serialized_start=20284 + _globals['_CACHESTATUS']._serialized_end=20318 + _globals['_BACKUPKEYTYPE']._serialized_start=20321 + _globals['_BACKUPKEYTYPE']._serialized_end=20468 + _globals['_BACKUPUSERDATAKEYTYPE']._serialized_start=20470 + _globals['_BACKUPUSERDATAKEYTYPE']._serialized_end=20528 + _globals['_ENCRYPTEDKEYTYPE']._serialized_start=20531 + _globals['_ENCRYPTEDKEYTYPE']._serialized_end=20696 + _globals['_ENTERPRISEFLAGTYPE']._serialized_start=20699 + _globals['_ENTERPRISEFLAGTYPE']._serialized_end=20969 + _globals['_USERUPDATESTATUS']._serialized_start=20971 + _globals['_USERUPDATESTATUS']._serialized_end=21040 + _globals['_AUDITUSERSTATUS']._serialized_start=21042 + _globals['_AUDITUSERSTATUS']._serialized_end=21115 + _globals['_TEAMUSERTYPE']._serialized_start=21117 + _globals['_TEAMUSERTYPE']._serialized_end=21168 + _globals['_APPCLIENTTYPE']._serialized_start=21170 + _globals['_APPCLIENTTYPE']._serialized_end=21290 + _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_start=21293 + _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_end=21436 + _globals['_CLEARSECURITYDATATYPE']._serialized_start=21439 + _globals['_CLEARSECURITYDATATYPE']._serialized_end=21574 _globals['_ENTERPRISEKEYPAIRREQUEST']._serialized_start=33 _globals['_ENTERPRISEKEYPAIRREQUEST']._serialized_end=165 _globals['_GETTEAMMEMBERREQUEST']._serialized_start=167 @@ -115,265 +107,265 @@ _globals['_ROLEUSERSREMOVERESPONSE']._serialized_start=1969 _globals['_ROLEUSERSREMOVERESPONSE']._serialized_end=2045 _globals['_ENTERPRISEREGISTRATION']._serialized_start=2048 - _globals['_ENTERPRISEREGISTRATION']._serialized_end=2566 - _globals['_DOMAINPASSWORDRULESREQUEST']._serialized_start=2568 - _globals['_DOMAINPASSWORDRULESREQUEST']._serialized_end=2640 - _globals['_DOMAINPASSWORDRULESFIELDS']._serialized_start=2642 - _globals['_DOMAINPASSWORDRULESFIELDS']._serialized_end=2734 - _globals['_LOGINTOMCREQUEST']._serialized_start=2736 - _globals['_LOGINTOMCREQUEST']._serialized_end=2805 - _globals['_LOGINTOMCRESPONSE']._serialized_start=2807 - _globals['_LOGINTOMCRESPONSE']._serialized_end=2883 - _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_start=2885 - _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_end=2988 - _globals['_APPROVEUSERDEVICEREQUEST']._serialized_start=2991 - _globals['_APPROVEUSERDEVICEREQUEST']._serialized_end=3127 - _globals['_APPROVEUSERDEVICERESPONSE']._serialized_start=3129 - _globals['_APPROVEUSERDEVICERESPONSE']._serialized_end=3245 - _globals['_APPROVEUSERDEVICESREQUEST']._serialized_start=3247 - _globals['_APPROVEUSERDEVICESREQUEST']._serialized_end=3336 - _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_start=3338 - _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_end=3430 - _globals['_ENTERPRISEUSERDATAKEY']._serialized_start=3433 - _globals['_ENTERPRISEUSERDATAKEY']._serialized_end=3568 - _globals['_ENTERPRISEUSERDATAKEYS']._serialized_start=3570 - _globals['_ENTERPRISEUSERDATAKEYS']._serialized_end=3643 - _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_start=3645 - _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_end=3748 - _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_start=3750 - _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_end=3850 - _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_start=3852 - _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_end=3946 - _globals['_ENTERPRISEDATAREQUEST']._serialized_start=3948 - _globals['_ENTERPRISEDATAREQUEST']._serialized_end=3998 - _globals['_SPECIALPROVISIONING']._serialized_start=4000 - _globals['_SPECIALPROVISIONING']._serialized_end=4048 - _globals['_GENERALDATAENTITY']._serialized_start=4051 - _globals['_GENERALDATAENTITY']._serialized_end=4311 - _globals['_NODE']._serialized_start=4314 - _globals['_NODE']._serialized_end=4567 - _globals['_ROLE']._serialized_start=4570 - _globals['_ROLE']._serialized_end=4712 - _globals['_USER']._serialized_start=4715 - _globals['_USER']._serialized_end=5027 - _globals['_USERALIAS']._serialized_start=5029 - _globals['_USERALIAS']._serialized_end=5084 - _globals['_COMPLIANCEREPORTMETADATA']._serialized_start=5087 - _globals['_COMPLIANCEREPORTMETADATA']._serialized_end=5259 - _globals['_MANAGEDNODE']._serialized_start=5261 - _globals['_MANAGEDNODE']._serialized_end=5344 - _globals['_USERMANAGEDNODE']._serialized_start=5346 - _globals['_USERMANAGEDNODE']._serialized_end=5430 - _globals['_USERPRIVILEGE']._serialized_start=5432 - _globals['_USERPRIVILEGE']._serialized_end=5551 - _globals['_ROLEUSER']._serialized_start=5553 - _globals['_ROLEUSER']._serialized_end=5605 - _globals['_ROLEPRIVILEGE']._serialized_start=5607 - _globals['_ROLEPRIVILEGE']._serialized_end=5684 - _globals['_ROLEENFORCEMENT']._serialized_start=5686 - _globals['_ROLEENFORCEMENT']._serialized_end=5759 - _globals['_TEAM']._serialized_start=5762 - _globals['_TEAM']._serialized_end=5931 - _globals['_TEAMUSER']._serialized_start=5933 - _globals['_TEAMUSER']._serialized_end=6004 - _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_start=6006 - _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_end=6081 - _globals['_DISTRIBUTOR']._serialized_start=6083 - _globals['_DISTRIBUTOR']._serialized_end=6149 - _globals['_MSPINFO']._serialized_start=6152 - _globals['_MSPINFO']._serialized_end=6437 - _globals['_MANAGEDCOMPANY']._serialized_start=6440 - _globals['_MANAGEDCOMPANY']._serialized_end=6713 - _globals['_MSPPOOL']._serialized_start=6715 - _globals['_MSPPOOL']._serialized_end=6797 - _globals['_MSPCONTACT']._serialized_start=6799 - _globals['_MSPCONTACT']._serialized_end=6857 - _globals['_LICENSEADDON']._serialized_start=6860 - _globals['_LICENSEADDON']._serialized_end=7096 - _globals['_MCDEFAULT']._serialized_start=7098 - _globals['_MCDEFAULT']._serialized_end=7213 - _globals['_MSPPERMITS']._serialized_start=7216 - _globals['_MSPPERMITS']._serialized_end=7426 - _globals['_LICENSE']._serialized_start=7429 - _globals['_LICENSE']._serialized_end=7973 - _globals['_BRIDGE']._serialized_start=7975 - _globals['_BRIDGE']._serialized_end=8085 - _globals['_SCIM']._serialized_start=8087 - _globals['_SCIM']._serialized_end=8203 - _globals['_EMAILPROVISION']._serialized_start=8205 - _globals['_EMAILPROVISION']._serialized_end=8281 - _globals['_QUEUEDTEAM']._serialized_start=8283 - _globals['_QUEUEDTEAM']._serialized_end=8365 - _globals['_QUEUEDTEAMUSER']._serialized_start=8367 - _globals['_QUEUEDTEAMUSER']._serialized_end=8415 - _globals['_TEAMSADDRESULT']._serialized_start=8418 - _globals['_TEAMSADDRESULT']._serialized_end=8582 - _globals['_TEAMADDRESULT']._serialized_start=8584 - _globals['_TEAMADDRESULT']._serialized_end=8669 - _globals['_SSOSERVICE']._serialized_start=8672 - _globals['_SSOSERVICE']._serialized_end=8817 - _globals['_REPORTFILTERUSER']._serialized_start=8819 - _globals['_REPORTFILTERUSER']._serialized_end=8868 - _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_start=8871 - _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_end=9150 - _globals['_ENTERPRISEDATA']._serialized_start=9152 - _globals['_ENTERPRISEDATA']._serialized_end=9248 - _globals['_ENTERPRISEDATARESPONSE']._serialized_start=9251 - _globals['_ENTERPRISEDATARESPONSE']._serialized_end=9459 - _globals['_BACKUPREQUEST']._serialized_start=9461 - _globals['_BACKUPREQUEST']._serialized_end=9503 - _globals['_BACKUPRECORD']._serialized_start=9506 - _globals['_BACKUPRECORD']._serialized_end=9658 - _globals['_BACKUPKEY']._serialized_start=9660 - _globals['_BACKUPKEY']._serialized_end=9706 - _globals['_BACKUPUSER']._serialized_start=9709 - _globals['_BACKUPUSER']._serialized_end=9978 - _globals['_BACKUPRESPONSE']._serialized_start=9981 - _globals['_BACKUPRESPONSE']._serialized_end=10139 - _globals['_BACKUPFILE']._serialized_start=10141 - _globals['_BACKUPFILE']._serialized_end=10242 - _globals['_BACKUPSRESPONSE']._serialized_start=10244 - _globals['_BACKUPSRESPONSE']._serialized_end=10300 - _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_start=10302 - _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_end=10348 - _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_start=10351 - _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_end=10606 - _globals['_ROLEKEY']._serialized_start=10608 - _globals['_ROLEKEY']._serialized_end=10702 - _globals['_MSPKEY']._serialized_start=10704 - _globals['_MSPKEY']._serialized_end=10804 - _globals['_ENTERPRISEKEYS']._serialized_start=10806 - _globals['_ENTERPRISEKEYS']._serialized_end=10930 - _globals['_TREEKEY']._serialized_start=10932 - _globals['_TREEKEY']._serialized_end=11004 - _globals['_SHAREDRECORDRESPONSE']._serialized_start=11006 - _globals['_SHAREDRECORDRESPONSE']._serialized_end=11075 - _globals['_SHAREDRECORDEVENT']._serialized_start=11077 - _globals['_SHAREDRECORDEVENT']._serialized_end=11189 - _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_start=11191 - _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_end=11237 - _globals['_USERADDREQUEST']._serialized_start=11240 - _globals['_USERADDREQUEST']._serialized_end=11448 - _globals['_USERUPDATEREQUEST']._serialized_start=11450 - _globals['_USERUPDATEREQUEST']._serialized_end=11508 - _globals['_USERUPDATE']._serialized_start=11511 - _globals['_USERUPDATE']._serialized_end=11686 - _globals['_USERUPDATERESPONSE']._serialized_start=11688 - _globals['_USERUPDATERESPONSE']._serialized_end=11753 - _globals['_USERUPDATERESULT']._serialized_start=11755 - _globals['_USERUPDATERESULT']._serialized_end=11845 - _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_start=11847 - _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_end=11921 - _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_start=11923 - _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_end=12002 - _globals['_RECORDOWNER']._serialized_start=12004 - _globals['_RECORDOWNER']._serialized_end=12059 - _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_start=12062 - _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_end=12228 - _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_start=12231 - _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_end=12390 - _globals['_AUDITUSERRECORD']._serialized_start=12392 - _globals['_AUDITUSERRECORD']._serialized_end=12467 - _globals['_AUDITUSERDATA']._serialized_start=12470 - _globals['_AUDITUSERDATA']._serialized_end=12611 - _globals['_COMPLIANCEREPORTFILTERS']._serialized_start=12613 - _globals['_COMPLIANCEREPORTFILTERS']._serialized_end=12740 - _globals['_COMPLIANCEREPORTREQUEST']._serialized_start=12742 - _globals['_COMPLIANCEREPORTREQUEST']._serialized_end=12869 - _globals['_COMPLIANCEREPORTRUN']._serialized_start=12872 - _globals['_COMPLIANCEREPORTRUN']._serialized_end=13005 - _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_start=13008 - _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_end=13260 - _globals['_COMPLIANCEREPORTCRITERIA']._serialized_start=13262 - _globals['_COMPLIANCEREPORTCRITERIA']._serialized_end=13360 - _globals['_COMPLIANCEREPORTFILTER']._serialized_start=13362 - _globals['_COMPLIANCEREPORTFILTER']._serialized_end=13482 - _globals['_COMPLIANCEREPORTRESPONSE']._serialized_start=13485 - _globals['_COMPLIANCEREPORTRESPONSE']._serialized_end=14158 - _globals['_AUDITRECORD']._serialized_start=14161 - _globals['_AUDITRECORD']._serialized_end=14290 - _globals['_AUDITROLE']._serialized_start=14293 - _globals['_AUDITROLE']._serialized_end=14549 - _globals['_ROLENODEMANAGEMENT']._serialized_start=14551 - _globals['_ROLENODEMANAGEMENT']._serialized_end=14645 - _globals['_USERPROFILE']._serialized_start=14647 - _globals['_USERPROFILE']._serialized_end=14754 - _globals['_RECORDPERMISSION']._serialized_start=14756 - _globals['_RECORDPERMISSION']._serialized_end=14817 - _globals['_USERRECORD']._serialized_start=14819 - _globals['_USERRECORD']._serialized_end=14914 - _globals['_AUDITTEAM']._serialized_start=14916 - _globals['_AUDITTEAM']._serialized_end=15007 - _globals['_AUDITTEAMUSER']._serialized_start=15009 - _globals['_AUDITTEAMUSER']._serialized_end=15068 - _globals['_SHAREDFOLDERRECORD']._serialized_start=15071 - _globals['_SHAREDFOLDERRECORD']._serialized_end=15230 - _globals['_SHAREADMINRECORD']._serialized_start=15232 - _globals['_SHAREADMINRECORD']._serialized_end=15309 - _globals['_SHAREDFOLDERUSER']._serialized_start=15311 - _globals['_SHAREDFOLDERUSER']._serialized_end=15381 - _globals['_SHAREDFOLDERTEAM']._serialized_start=15383 - _globals['_SHAREDFOLDERTEAM']._serialized_end=15444 - _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_start=15446 - _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_end=15493 - _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_start=15495 - _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_end=15545 - _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_start=15547 - _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_end=15601 - _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_start=15603 - _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_end=15662 - _globals['_LINKEDRECORD']._serialized_start=15664 - _globals['_LINKEDRECORD']._serialized_end=15716 - _globals['_GETSHARINGADMINSREQUEST']._serialized_start=15718 - _globals['_GETSHARINGADMINSREQUEST']._serialized_end=15805 - _globals['_USERPROFILEEXT']._serialized_start=15808 - _globals['_USERPROFILEEXT']._serialized_end=16032 - _globals['_GETSHARINGADMINSRESPONSE']._serialized_start=16034 - _globals['_GETSHARINGADMINSRESPONSE']._serialized_end=16113 - _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_start=16115 - _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_end=16210 - _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_start=16212 - _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_end=16328 - _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_start=16331 - _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_end=16502 - _globals['_TYPEDKEY']._serialized_start=16504 - _globals['_TYPEDKEY']._serialized_end=16574 - _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_start=16576 - _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_end=16691 - _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_start=16694 - _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_end=16890 - _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_start=16893 - _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_end=17052 - _globals['_DOMAINALIAS']._serialized_start=17054 - _globals['_DOMAINALIAS']._serialized_end=17131 - _globals['_DOMAINALIASREQUEST']._serialized_start=17133 - _globals['_DOMAINALIASREQUEST']._serialized_end=17199 - _globals['_DOMAINALIASRESPONSE']._serialized_start=17201 - _globals['_DOMAINALIASRESPONSE']._serialized_end=17268 - _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_start=17270 - _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_end=17379 - _globals['_ENTERPRISEUSERSPROVISION']._serialized_start=17382 - _globals['_ENTERPRISEUSERSPROVISION']._serialized_end=17820 - _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_start=17822 - _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_end=17917 - _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_start=17919 - _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_end=18032 - _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_start=18034 - _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_end=18131 - _globals['_ENTERPRISEUSERSADD']._serialized_start=18134 - _globals['_ENTERPRISEUSERSADD']._serialized_end=18402 - _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_start=18405 - _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_end=18560 - _globals['_ENTERPRISEUSERSADDRESULT']._serialized_start=18563 - _globals['_ENTERPRISEUSERSADDRESULT']._serialized_end=18713 - _globals['_UPDATEMSPPERMITSREQUEST']._serialized_start=18716 - _globals['_UPDATEMSPPERMITSREQUEST']._serialized_end=18901 - _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_start=18903 - _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_end=18960 - _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_start=18962 - _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_end=19073 - _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_start=19075 - _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_end=19168 - _globals['_CLEARSECURITYDATAREQUEST']._serialized_start=19170 - _globals['_CLEARSECURITYDATAREQUEST']._serialized_end=19289 + _globals['_ENTERPRISEREGISTRATION']._serialized_end=2592 + _globals['_DOMAINPASSWORDRULESREQUEST']._serialized_start=2594 + _globals['_DOMAINPASSWORDRULESREQUEST']._serialized_end=2666 + _globals['_DOMAINPASSWORDRULESFIELDS']._serialized_start=2668 + _globals['_DOMAINPASSWORDRULESFIELDS']._serialized_end=2760 + _globals['_LOGINTOMCREQUEST']._serialized_start=2762 + _globals['_LOGINTOMCREQUEST']._serialized_end=2831 + _globals['_LOGINTOMCRESPONSE']._serialized_start=2833 + _globals['_LOGINTOMCRESPONSE']._serialized_end=2909 + _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_start=2911 + _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_end=3014 + _globals['_APPROVEUSERDEVICEREQUEST']._serialized_start=3017 + _globals['_APPROVEUSERDEVICEREQUEST']._serialized_end=3153 + _globals['_APPROVEUSERDEVICERESPONSE']._serialized_start=3155 + _globals['_APPROVEUSERDEVICERESPONSE']._serialized_end=3271 + _globals['_APPROVEUSERDEVICESREQUEST']._serialized_start=3273 + _globals['_APPROVEUSERDEVICESREQUEST']._serialized_end=3362 + _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_start=3364 + _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_end=3456 + _globals['_ENTERPRISEUSERDATAKEY']._serialized_start=3459 + _globals['_ENTERPRISEUSERDATAKEY']._serialized_end=3594 + _globals['_ENTERPRISEUSERDATAKEYS']._serialized_start=3596 + _globals['_ENTERPRISEUSERDATAKEYS']._serialized_end=3669 + _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_start=3671 + _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_end=3774 + _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_start=3776 + _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_end=3876 + _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_start=3878 + _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_end=3972 + _globals['_ENTERPRISEDATAREQUEST']._serialized_start=3974 + _globals['_ENTERPRISEDATAREQUEST']._serialized_end=4024 + _globals['_SPECIALPROVISIONING']._serialized_start=4026 + _globals['_SPECIALPROVISIONING']._serialized_end=4074 + _globals['_GENERALDATAENTITY']._serialized_start=4077 + _globals['_GENERALDATAENTITY']._serialized_end=4337 + _globals['_NODE']._serialized_start=4340 + _globals['_NODE']._serialized_end=4593 + _globals['_ROLE']._serialized_start=4596 + _globals['_ROLE']._serialized_end=4738 + _globals['_USER']._serialized_start=4741 + _globals['_USER']._serialized_end=5053 + _globals['_USERALIAS']._serialized_start=5055 + _globals['_USERALIAS']._serialized_end=5110 + _globals['_COMPLIANCEREPORTMETADATA']._serialized_start=5113 + _globals['_COMPLIANCEREPORTMETADATA']._serialized_end=5285 + _globals['_MANAGEDNODE']._serialized_start=5287 + _globals['_MANAGEDNODE']._serialized_end=5370 + _globals['_USERMANAGEDNODE']._serialized_start=5372 + _globals['_USERMANAGEDNODE']._serialized_end=5456 + _globals['_USERPRIVILEGE']._serialized_start=5458 + _globals['_USERPRIVILEGE']._serialized_end=5577 + _globals['_ROLEUSER']._serialized_start=5579 + _globals['_ROLEUSER']._serialized_end=5631 + _globals['_ROLEPRIVILEGE']._serialized_start=5633 + _globals['_ROLEPRIVILEGE']._serialized_end=5710 + _globals['_ROLEENFORCEMENT']._serialized_start=5712 + _globals['_ROLEENFORCEMENT']._serialized_end=5785 + _globals['_TEAM']._serialized_start=5788 + _globals['_TEAM']._serialized_end=5957 + _globals['_TEAMUSER']._serialized_start=5959 + _globals['_TEAMUSER']._serialized_end=6030 + _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_start=6032 + _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_end=6107 + _globals['_DISTRIBUTOR']._serialized_start=6109 + _globals['_DISTRIBUTOR']._serialized_end=6175 + _globals['_MSPINFO']._serialized_start=6178 + _globals['_MSPINFO']._serialized_end=6463 + _globals['_MANAGEDCOMPANY']._serialized_start=6466 + _globals['_MANAGEDCOMPANY']._serialized_end=6739 + _globals['_MSPPOOL']._serialized_start=6741 + _globals['_MSPPOOL']._serialized_end=6823 + _globals['_MSPCONTACT']._serialized_start=6825 + _globals['_MSPCONTACT']._serialized_end=6883 + _globals['_LICENSEADDON']._serialized_start=6886 + _globals['_LICENSEADDON']._serialized_end=7122 + _globals['_MCDEFAULT']._serialized_start=7124 + _globals['_MCDEFAULT']._serialized_end=7239 + _globals['_MSPPERMITS']._serialized_start=7242 + _globals['_MSPPERMITS']._serialized_end=7452 + _globals['_LICENSE']._serialized_start=7455 + _globals['_LICENSE']._serialized_end=7999 + _globals['_BRIDGE']._serialized_start=8001 + _globals['_BRIDGE']._serialized_end=8111 + _globals['_SCIM']._serialized_start=8113 + _globals['_SCIM']._serialized_end=8229 + _globals['_EMAILPROVISION']._serialized_start=8231 + _globals['_EMAILPROVISION']._serialized_end=8307 + _globals['_QUEUEDTEAM']._serialized_start=8309 + _globals['_QUEUEDTEAM']._serialized_end=8391 + _globals['_QUEUEDTEAMUSER']._serialized_start=8393 + _globals['_QUEUEDTEAMUSER']._serialized_end=8441 + _globals['_TEAMSADDRESULT']._serialized_start=8444 + _globals['_TEAMSADDRESULT']._serialized_end=8608 + _globals['_TEAMADDRESULT']._serialized_start=8610 + _globals['_TEAMADDRESULT']._serialized_end=8695 + _globals['_SSOSERVICE']._serialized_start=8698 + _globals['_SSOSERVICE']._serialized_end=8843 + _globals['_REPORTFILTERUSER']._serialized_start=8845 + _globals['_REPORTFILTERUSER']._serialized_end=8894 + _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_start=8897 + _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_end=9176 + _globals['_ENTERPRISEDATA']._serialized_start=9178 + _globals['_ENTERPRISEDATA']._serialized_end=9274 + _globals['_ENTERPRISEDATARESPONSE']._serialized_start=9277 + _globals['_ENTERPRISEDATARESPONSE']._serialized_end=9485 + _globals['_BACKUPREQUEST']._serialized_start=9487 + _globals['_BACKUPREQUEST']._serialized_end=9529 + _globals['_BACKUPRECORD']._serialized_start=9532 + _globals['_BACKUPRECORD']._serialized_end=9684 + _globals['_BACKUPKEY']._serialized_start=9686 + _globals['_BACKUPKEY']._serialized_end=9732 + _globals['_BACKUPUSER']._serialized_start=9735 + _globals['_BACKUPUSER']._serialized_end=10004 + _globals['_BACKUPRESPONSE']._serialized_start=10007 + _globals['_BACKUPRESPONSE']._serialized_end=10165 + _globals['_BACKUPFILE']._serialized_start=10167 + _globals['_BACKUPFILE']._serialized_end=10268 + _globals['_BACKUPSRESPONSE']._serialized_start=10270 + _globals['_BACKUPSRESPONSE']._serialized_end=10326 + _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_start=10328 + _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_end=10374 + _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_start=10377 + _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_end=10632 + _globals['_ROLEKEY']._serialized_start=10634 + _globals['_ROLEKEY']._serialized_end=10728 + _globals['_MSPKEY']._serialized_start=10730 + _globals['_MSPKEY']._serialized_end=10830 + _globals['_ENTERPRISEKEYS']._serialized_start=10832 + _globals['_ENTERPRISEKEYS']._serialized_end=10956 + _globals['_TREEKEY']._serialized_start=10958 + _globals['_TREEKEY']._serialized_end=11030 + _globals['_SHAREDRECORDRESPONSE']._serialized_start=11032 + _globals['_SHAREDRECORDRESPONSE']._serialized_end=11101 + _globals['_SHAREDRECORDEVENT']._serialized_start=11103 + _globals['_SHAREDRECORDEVENT']._serialized_end=11215 + _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_start=11217 + _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_end=11263 + _globals['_USERADDREQUEST']._serialized_start=11266 + _globals['_USERADDREQUEST']._serialized_end=11474 + _globals['_USERUPDATEREQUEST']._serialized_start=11476 + _globals['_USERUPDATEREQUEST']._serialized_end=11534 + _globals['_USERUPDATE']._serialized_start=11537 + _globals['_USERUPDATE']._serialized_end=11712 + _globals['_USERUPDATERESPONSE']._serialized_start=11714 + _globals['_USERUPDATERESPONSE']._serialized_end=11779 + _globals['_USERUPDATERESULT']._serialized_start=11781 + _globals['_USERUPDATERESULT']._serialized_end=11871 + _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_start=11873 + _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_end=11947 + _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_start=11949 + _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_end=12028 + _globals['_RECORDOWNER']._serialized_start=12030 + _globals['_RECORDOWNER']._serialized_end=12085 + _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_start=12088 + _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_end=12254 + _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_start=12257 + _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_end=12416 + _globals['_AUDITUSERRECORD']._serialized_start=12418 + _globals['_AUDITUSERRECORD']._serialized_end=12493 + _globals['_AUDITUSERDATA']._serialized_start=12496 + _globals['_AUDITUSERDATA']._serialized_end=12637 + _globals['_COMPLIANCEREPORTFILTERS']._serialized_start=12639 + _globals['_COMPLIANCEREPORTFILTERS']._serialized_end=12766 + _globals['_COMPLIANCEREPORTREQUEST']._serialized_start=12768 + _globals['_COMPLIANCEREPORTREQUEST']._serialized_end=12895 + _globals['_COMPLIANCEREPORTRUN']._serialized_start=12898 + _globals['_COMPLIANCEREPORTRUN']._serialized_end=13031 + _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_start=13034 + _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_end=13286 + _globals['_COMPLIANCEREPORTCRITERIA']._serialized_start=13288 + _globals['_COMPLIANCEREPORTCRITERIA']._serialized_end=13386 + _globals['_COMPLIANCEREPORTFILTER']._serialized_start=13388 + _globals['_COMPLIANCEREPORTFILTER']._serialized_end=13508 + _globals['_COMPLIANCEREPORTRESPONSE']._serialized_start=13511 + _globals['_COMPLIANCEREPORTRESPONSE']._serialized_end=14184 + _globals['_AUDITRECORD']._serialized_start=14187 + _globals['_AUDITRECORD']._serialized_end=14316 + _globals['_AUDITROLE']._serialized_start=14319 + _globals['_AUDITROLE']._serialized_end=14575 + _globals['_ROLENODEMANAGEMENT']._serialized_start=14577 + _globals['_ROLENODEMANAGEMENT']._serialized_end=14671 + _globals['_USERPROFILE']._serialized_start=14673 + _globals['_USERPROFILE']._serialized_end=14780 + _globals['_RECORDPERMISSION']._serialized_start=14782 + _globals['_RECORDPERMISSION']._serialized_end=14843 + _globals['_USERRECORD']._serialized_start=14845 + _globals['_USERRECORD']._serialized_end=14940 + _globals['_AUDITTEAM']._serialized_start=14942 + _globals['_AUDITTEAM']._serialized_end=15033 + _globals['_AUDITTEAMUSER']._serialized_start=15035 + _globals['_AUDITTEAMUSER']._serialized_end=15094 + _globals['_SHAREDFOLDERRECORD']._serialized_start=15097 + _globals['_SHAREDFOLDERRECORD']._serialized_end=15256 + _globals['_SHAREADMINRECORD']._serialized_start=15258 + _globals['_SHAREADMINRECORD']._serialized_end=15335 + _globals['_SHAREDFOLDERUSER']._serialized_start=15337 + _globals['_SHAREDFOLDERUSER']._serialized_end=15407 + _globals['_SHAREDFOLDERTEAM']._serialized_start=15409 + _globals['_SHAREDFOLDERTEAM']._serialized_end=15470 + _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_start=15472 + _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_end=15519 + _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_start=15521 + _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_end=15571 + _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_start=15573 + _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_end=15627 + _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_start=15629 + _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_end=15688 + _globals['_LINKEDRECORD']._serialized_start=15690 + _globals['_LINKEDRECORD']._serialized_end=15742 + _globals['_GETSHARINGADMINSREQUEST']._serialized_start=15744 + _globals['_GETSHARINGADMINSREQUEST']._serialized_end=15831 + _globals['_USERPROFILEEXT']._serialized_start=15834 + _globals['_USERPROFILEEXT']._serialized_end=16058 + _globals['_GETSHARINGADMINSRESPONSE']._serialized_start=16060 + _globals['_GETSHARINGADMINSRESPONSE']._serialized_end=16139 + _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_start=16141 + _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_end=16236 + _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_start=16238 + _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_end=16354 + _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_start=16357 + _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_end=16528 + _globals['_TYPEDKEY']._serialized_start=16530 + _globals['_TYPEDKEY']._serialized_end=16600 + _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_start=16602 + _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_end=16717 + _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_start=16720 + _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_end=16916 + _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_start=16919 + _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_end=17078 + _globals['_DOMAINALIAS']._serialized_start=17080 + _globals['_DOMAINALIAS']._serialized_end=17157 + _globals['_DOMAINALIASREQUEST']._serialized_start=17159 + _globals['_DOMAINALIASREQUEST']._serialized_end=17225 + _globals['_DOMAINALIASRESPONSE']._serialized_start=17227 + _globals['_DOMAINALIASRESPONSE']._serialized_end=17294 + _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_start=17296 + _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_end=17405 + _globals['_ENTERPRISEUSERSPROVISION']._serialized_start=17408 + _globals['_ENTERPRISEUSERSPROVISION']._serialized_end=17846 + _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_start=17848 + _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_end=17943 + _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_start=17945 + _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_end=18058 + _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_start=18060 + _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_end=18157 + _globals['_ENTERPRISEUSERSADD']._serialized_start=18160 + _globals['_ENTERPRISEUSERSADD']._serialized_end=18428 + _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_start=18431 + _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_end=18586 + _globals['_ENTERPRISEUSERSADDRESULT']._serialized_start=18589 + _globals['_ENTERPRISEUSERSADDRESULT']._serialized_end=18739 + _globals['_UPDATEMSPPERMITSREQUEST']._serialized_start=18742 + _globals['_UPDATEMSPPERMITSREQUEST']._serialized_end=18927 + _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_start=18929 + _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_end=18986 + _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_start=18988 + _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_end=19099 + _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_start=19101 + _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_end=19194 + _globals['_CLEARSECURITYDATAREQUEST']._serialized_start=19196 + _globals['_CLEARSECURITYDATAREQUEST']._serialized_end=19315 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi index dfa01916..3cf20bc2 100644 --- a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi @@ -421,7 +421,7 @@ class RoleUsersRemoveResponse(_message.Message): def __init__(self, results: _Optional[_Iterable[_Union[RoleUserRemoveResult, _Mapping]]] = ...) -> None: ... class EnterpriseRegistration(_message.Message): - __slots__ = ("encryptedTreeKey", "enterpriseName", "rootNodeData", "adminUserData", "adminName", "roleData", "rsaKeyPair", "numberSeats", "enterpriseType", "rolePublicKey", "rolePrivateKeyEncryptedWithRoleKey", "roleKeyEncryptedWithTreeKey", "eccKeyPair", "allUsersRoleData", "roleKeyEncryptedWithUserPublicKey") + __slots__ = ("encryptedTreeKey", "enterpriseName", "rootNodeData", "adminUserData", "adminName", "roleData", "rsaKeyPair", "numberSeats", "enterpriseType", "rolePublicKey", "rolePrivateKeyEncryptedWithRoleKey", "roleKeyEncryptedWithTreeKey", "eccKeyPair", "allUsersRoleData", "roleKeyEncryptedWithUserPublicKey", "approverRoleData") ENCRYPTEDTREEKEY_FIELD_NUMBER: _ClassVar[int] ENTERPRISENAME_FIELD_NUMBER: _ClassVar[int] ROOTNODEDATA_FIELD_NUMBER: _ClassVar[int] @@ -437,6 +437,7 @@ class EnterpriseRegistration(_message.Message): ECCKEYPAIR_FIELD_NUMBER: _ClassVar[int] ALLUSERSROLEDATA_FIELD_NUMBER: _ClassVar[int] ROLEKEYENCRYPTEDWITHUSERPUBLICKEY_FIELD_NUMBER: _ClassVar[int] + APPROVERROLEDATA_FIELD_NUMBER: _ClassVar[int] encryptedTreeKey: bytes enterpriseName: str rootNodeData: bytes @@ -452,7 +453,8 @@ class EnterpriseRegistration(_message.Message): eccKeyPair: EnterpriseKeyPairRequest allUsersRoleData: bytes roleKeyEncryptedWithUserPublicKey: bytes - def __init__(self, encryptedTreeKey: _Optional[bytes] = ..., enterpriseName: _Optional[str] = ..., rootNodeData: _Optional[bytes] = ..., adminUserData: _Optional[bytes] = ..., adminName: _Optional[str] = ..., roleData: _Optional[bytes] = ..., rsaKeyPair: _Optional[_Union[EnterpriseKeyPairRequest, _Mapping]] = ..., numberSeats: _Optional[int] = ..., enterpriseType: _Optional[_Union[EnterpriseType, str]] = ..., rolePublicKey: _Optional[bytes] = ..., rolePrivateKeyEncryptedWithRoleKey: _Optional[bytes] = ..., roleKeyEncryptedWithTreeKey: _Optional[bytes] = ..., eccKeyPair: _Optional[_Union[EnterpriseKeyPairRequest, _Mapping]] = ..., allUsersRoleData: _Optional[bytes] = ..., roleKeyEncryptedWithUserPublicKey: _Optional[bytes] = ...) -> None: ... + approverRoleData: bytes + def __init__(self, encryptedTreeKey: _Optional[bytes] = ..., enterpriseName: _Optional[str] = ..., rootNodeData: _Optional[bytes] = ..., adminUserData: _Optional[bytes] = ..., adminName: _Optional[str] = ..., roleData: _Optional[bytes] = ..., rsaKeyPair: _Optional[_Union[EnterpriseKeyPairRequest, _Mapping]] = ..., numberSeats: _Optional[int] = ..., enterpriseType: _Optional[_Union[EnterpriseType, str]] = ..., rolePublicKey: _Optional[bytes] = ..., rolePrivateKeyEncryptedWithRoleKey: _Optional[bytes] = ..., roleKeyEncryptedWithTreeKey: _Optional[bytes] = ..., eccKeyPair: _Optional[_Union[EnterpriseKeyPairRequest, _Mapping]] = ..., allUsersRoleData: _Optional[bytes] = ..., roleKeyEncryptedWithUserPublicKey: _Optional[bytes] = ..., approverRoleData: _Optional[bytes] = ...) -> None: ... class DomainPasswordRulesRequest(_message.Message): __slots__ = ("username", "verificationCode") diff --git a/keepersdk-package/src/keepersdk/proto/folder_pb2.py b/keepersdk-package/src/keepersdk/proto/folder_pb2.py index 04efa866..ac2705f6 100644 --- a/keepersdk-package/src/keepersdk/proto/folder_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/folder_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: folder.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'folder.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/pam_pb2.py b/keepersdk-package/src/keepersdk/proto/pam_pb2.py index da667ff9..9d7bd4b1 100644 --- a/keepersdk-package/src/keepersdk/proto/pam_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/pam_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: pam.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'pam.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -26,7 +18,7 @@ from . import record_pb2 as record__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tpam.proto\x12\x03PAM\x1a\x10\x65nterprise.proto\x1a\x0crecord.proto\"\x83\x01\n\x13PAMRotationSchedule\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x14\n\x0cscheduleData\x18\x04 \x01(\t\x12\x12\n\nnoSchedule\x18\x05 \x01(\x08\"K\n\x1cPAMRotationSchedulesResponse\x12+\n\tschedules\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRotationSchedule\"\x94\x01\n\x13PAMOnlineController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63onnectedOn\x18\x02 \x01(\x03\x12\x11\n\tipAddress\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12-\n\x0b\x63onnections\x18\x05 \x03(\x0b\x32\x18.PAM.PAMWebRtcConnection\"\xa7\x01\n\x13PAMWebRtcConnection\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\'\n\x04type\x18\x02 \x01(\x0e\x32\x19.PAM.WebRtcConnectionType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x06 \x01(\x0c\"Y\n\x14PAMOnlineControllers\x12\x12\n\ndeprecated\x18\x01 \x03(\x0c\x12-\n\x0b\x63ontrollers\x18\x02 \x03(\x0b\x32\x18.PAM.PAMOnlineController\"9\n\x10PAMRotateRequest\x12\x12\n\nrequestUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\"A\n\x16PAMControllersResponse\x12\'\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x12.PAM.PAMController\"=\n\x13PAMRemoveController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"L\n\x1bPAMRemoveControllerResponse\x12-\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRemoveController\"=\n\x10PAMModifyRequest\x12)\n\noperations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMDataOperation\"\x98\x01\n\x10PAMDataOperation\x12,\n\roperationType\x18\x01 \x01(\x0e\x32\x15.PAM.PAMOperationType\x12\x30\n\rconfiguration\x18\x02 \x01(\x0b\x32\x19.PAM.PAMConfigurationData\x12$\n\x07\x65lement\x18\x03 \x01(\x0b\x32\x13.PAM.PAMElementData\"e\n\x14PAMConfigurationData\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\"E\n\x0ePAMElementData\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"m\n\x19PAMElementOperationResult\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12+\n\x06result\x18\x02 \x01(\x0e\x32\x1b.PAM.PAMOperationResultType\x12\x0f\n\x07message\x18\x03 \x01(\t\"B\n\x0fPAMModifyResult\x12/\n\x07results\x18\x01 \x03(\x0b\x32\x1e.PAM.PAMElementOperationResult\"x\n\nPAMElement\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x03 \x01(\x03\x12\x14\n\x0clastModified\x18\x04 \x01(\x03\x12!\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x0f.PAM.PAMElement\"#\n\x14PAMGenericUidRequest\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\"%\n\x15PAMGenericUidsRequest\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"\xab\x01\n\x10PAMConfiguration\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\x14\n\x0clastModified\x18\x06 \x01(\x03\x12!\n\x08\x63hildren\x18\x07 \x03(\x0b\x32\x0f.PAM.PAMElement\"B\n\x11PAMConfigurations\x12-\n\x0e\x63onfigurations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMConfiguration\"\xff\x01\n\rPAMController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x03 \x01(\t\x12\x12\n\ndeviceName\x18\x04 \x01(\t\x12\x0e\n\x06nodeId\x18\x05 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x06 \x01(\x03\x12\x14\n\x0clastModified\x18\x07 \x01(\x03\x12\x16\n\x0e\x61pplicationUid\x18\x08 \x01(\x0c\x12\x30\n\rappClientType\x18\t \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x15\n\risInitialized\x18\n \x01(\x08\"%\n\x12\x43ontrollerResponse\x12\x0f\n\x07payload\x18\x01 \x01(\t\"M\n\x1aPAMConfigurationController\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x02 \x01(\x0c\"\xa3\x01\n\x17\x43onfigurationAddRequest\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12(\n\x0brecordLinks\x18\x04 \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\x05 \x01(\x0b\x32\x14.Records.RecordAudit\"J\n\x10RelayAccessCreds\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x12\n\nserverTime\x18\x03 \x01(\x03\"\xbf\x01\n\x0cPAMRecording\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12,\n\rrecordingType\x18\x02 \x01(\x0e\x32\x15.PAM.PAMRecordingType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x0e\n\x06length\x18\x06 \x01(\x05\x12\x10\n\x08\x66ileSize\x18\x07 \x01(\x03\x12\x10\n\x08protocol\x18\x08 \x01(\t\">\n\x15PAMRecordingsResponse\x12%\n\nrecordings\x18\x01 \x03(\x0b\x32\x11.PAM.PAMRecording\"%\n\x07PAMLink\x12\x0c\n\x04head\x18\x01 \x01(\x0c\x12\x0c\n\x04tail\x18\x02 \x01(\x0c\"*\n\x07PAMData\x12\x0e\n\x06vertex\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c*\x8e\x01\n\x14WebRtcConnectionType\x12\x0e\n\nCONNECTION\x10\x00\x12\n\n\x06TUNNEL\x10\x01\x12\x07\n\x03SSH\x10\x02\x12\x07\n\x03RDP\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x07\n\x03VNC\x10\x05\x12\n\n\x06TELNET\x10\x06\x12\t\n\x05MYSQL\x10\x07\x12\x0e\n\nSQL_SERVER\x10\x08\x12\x0e\n\nPOSTGRESQL\x10\t*@\n\x10PAMOperationType\x12\x07\n\x03\x41\x44\x44\x10\x00\x12\n\n\x06UPDATE\x10\x01\x12\x0b\n\x07REPLACE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*p\n\x16PAMOperationResultType\x12\x0f\n\x0bPOT_SUCCESS\x10\x00\x12\x15\n\x11POT_UNKNOWN_ERROR\x10\x01\x12\x16\n\x12POT_ALREADY_EXISTS\x10\x02\x12\x16\n\x12POT_DOES_NOT_EXIST\x10\x03*\\\n\x15\x43ontrollerMessageType\x12\x0f\n\x0b\x43MT_GENERAL\x10\x00\x12\x0e\n\nCMT_ROTATE\x10\x01\x12\x11\n\rCMT_DISCOVERY\x10\x02\x12\x0f\n\x0b\x43MT_CONNECT\x10\x03*E\n\x10PAMRecordingType\x12\x0f\n\x0bPRT_SESSION\x10\x00\x12\x12\n\x0ePRT_TYPESCRIPT\x10\x01\x12\x0c\n\x08PRT_TIME\x10\x02\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tpam.proto\x12\x03PAM\x1a\x10\x65nterprise.proto\x1a\x0crecord.proto\"\x83\x01\n\x13PAMRotationSchedule\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x14\n\x0cscheduleData\x18\x04 \x01(\t\x12\x12\n\nnoSchedule\x18\x05 \x01(\x08\"K\n\x1cPAMRotationSchedulesResponse\x12+\n\tschedules\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRotationSchedule\"\x94\x01\n\x13PAMOnlineController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63onnectedOn\x18\x02 \x01(\x03\x12\x11\n\tipAddress\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12-\n\x0b\x63onnections\x18\x05 \x03(\x0b\x32\x18.PAM.PAMWebRtcConnection\"\xa7\x01\n\x13PAMWebRtcConnection\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\'\n\x04type\x18\x02 \x01(\x0e\x32\x19.PAM.WebRtcConnectionType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x06 \x01(\x0c\"Y\n\x14PAMOnlineControllers\x12\x12\n\ndeprecated\x18\x01 \x03(\x0c\x12-\n\x0b\x63ontrollers\x18\x02 \x03(\x0b\x32\x18.PAM.PAMOnlineController\"9\n\x10PAMRotateRequest\x12\x12\n\nrequestUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\"A\n\x16PAMControllersResponse\x12\'\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x12.PAM.PAMController\"=\n\x13PAMRemoveController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"L\n\x1bPAMRemoveControllerResponse\x12-\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRemoveController\"=\n\x10PAMModifyRequest\x12)\n\noperations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMDataOperation\"\x98\x01\n\x10PAMDataOperation\x12,\n\roperationType\x18\x01 \x01(\x0e\x32\x15.PAM.PAMOperationType\x12\x30\n\rconfiguration\x18\x02 \x01(\x0b\x32\x19.PAM.PAMConfigurationData\x12$\n\x07\x65lement\x18\x03 \x01(\x0b\x32\x13.PAM.PAMElementData\"e\n\x14PAMConfigurationData\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\"E\n\x0ePAMElementData\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"m\n\x19PAMElementOperationResult\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12+\n\x06result\x18\x02 \x01(\x0e\x32\x1b.PAM.PAMOperationResultType\x12\x0f\n\x07message\x18\x03 \x01(\t\"B\n\x0fPAMModifyResult\x12/\n\x07results\x18\x01 \x03(\x0b\x32\x1e.PAM.PAMElementOperationResult\"x\n\nPAMElement\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x03 \x01(\x03\x12\x14\n\x0clastModified\x18\x04 \x01(\x03\x12!\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x0f.PAM.PAMElement\"#\n\x14PAMGenericUidRequest\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\"%\n\x15PAMGenericUidsRequest\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"\xab\x01\n\x10PAMConfiguration\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\x14\n\x0clastModified\x18\x06 \x01(\x03\x12!\n\x08\x63hildren\x18\x07 \x03(\x0b\x32\x0f.PAM.PAMElement\"B\n\x11PAMConfigurations\x12-\n\x0e\x63onfigurations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMConfiguration\"\xff\x01\n\rPAMController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x03 \x01(\t\x12\x12\n\ndeviceName\x18\x04 \x01(\t\x12\x0e\n\x06nodeId\x18\x05 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x06 \x01(\x03\x12\x14\n\x0clastModified\x18\x07 \x01(\x03\x12\x16\n\x0e\x61pplicationUid\x18\x08 \x01(\x0c\x12\x30\n\rappClientType\x18\t \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x15\n\risInitialized\x18\n \x01(\x08\"%\n\x12\x43ontrollerResponse\x12\x0f\n\x07payload\x18\x01 \x01(\t\"M\n\x1aPAMConfigurationController\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x02 \x01(\x0c\"\xa3\x01\n\x17\x43onfigurationAddRequest\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12(\n\x0brecordLinks\x18\x04 \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\x05 \x01(\x0b\x32\x14.Records.RecordAudit\"J\n\x10RelayAccessCreds\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x12\n\nserverTime\x18\x03 \x01(\x03\"\xbf\x01\n\x0cPAMRecording\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12,\n\rrecordingType\x18\x02 \x01(\x0e\x32\x15.PAM.PAMRecordingType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x0e\n\x06length\x18\x06 \x01(\x05\x12\x10\n\x08\x66ileSize\x18\x07 \x01(\x03\x12\x10\n\x08protocol\x18\x08 \x01(\t\">\n\x15PAMRecordingsResponse\x12%\n\nrecordings\x18\x01 \x03(\x0b\x32\x11.PAM.PAMRecording\"*\n\x07PAMData\x12\x0e\n\x06vertex\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"\x17\n\x07UidList\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"\xd0\x02\n\x11PAMResourceConfig\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\nnetworkUid\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\x15\n\x08\x61\x64minUid\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x12\x11\n\x04meta\x18\x04 \x01(\x0cH\x02\x88\x01\x01\x12\x1f\n\x12\x63onnectionSettings\x18\x05 \x01(\x0cH\x03\x88\x01\x01\x12\'\n\x0c\x63onnectUsers\x18\x06 \x01(\x0b\x32\x0c.PAM.UidListH\x04\x88\x01\x01\x12\x16\n\tdomainUid\x18\x07 \x01(\x0cH\x05\x88\x01\x01\x12\x18\n\x0bjitSettings\x18\x08 \x01(\x0cH\x06\x88\x01\x01\x42\r\n\x0b_networkUidB\x0b\n\t_adminUidB\x07\n\x05_metaB\x15\n\x13_connectionSettingsB\x0f\n\r_connectUsersB\x0c\n\n_domainUidB\x0e\n\x0c_jitSettings*\x8e\x01\n\x14WebRtcConnectionType\x12\x0e\n\nCONNECTION\x10\x00\x12\n\n\x06TUNNEL\x10\x01\x12\x07\n\x03SSH\x10\x02\x12\x07\n\x03RDP\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x07\n\x03VNC\x10\x05\x12\n\n\x06TELNET\x10\x06\x12\t\n\x05MYSQL\x10\x07\x12\x0e\n\nSQL_SERVER\x10\x08\x12\x0e\n\nPOSTGRESQL\x10\t*@\n\x10PAMOperationType\x12\x07\n\x03\x41\x44\x44\x10\x00\x12\n\n\x06UPDATE\x10\x01\x12\x0b\n\x07REPLACE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*p\n\x16PAMOperationResultType\x12\x0f\n\x0bPOT_SUCCESS\x10\x00\x12\x15\n\x11POT_UNKNOWN_ERROR\x10\x01\x12\x16\n\x12POT_ALREADY_EXISTS\x10\x02\x12\x16\n\x12POT_DOES_NOT_EXIST\x10\x03*\\\n\x15\x43ontrollerMessageType\x12\x0f\n\x0b\x43MT_GENERAL\x10\x00\x12\x0e\n\nCMT_ROTATE\x10\x01\x12\x11\n\rCMT_DISCOVERY\x10\x02\x12\x0f\n\x0b\x43MT_CONNECT\x10\x03*E\n\x10PAMRecordingType\x12\x0f\n\x0bPRT_SESSION\x10\x00\x12\x12\n\x0ePRT_TYPESCRIPT\x10\x01\x12\x0c\n\x08PRT_TIME\x10\x02\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,16 +26,16 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\003PAM' - _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=2911 - _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=3053 - _globals['_PAMOPERATIONTYPE']._serialized_start=3055 - _globals['_PAMOPERATIONTYPE']._serialized_end=3119 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=3121 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=3233 - _globals['_CONTROLLERMESSAGETYPE']._serialized_start=3235 - _globals['_CONTROLLERMESSAGETYPE']._serialized_end=3327 - _globals['_PAMRECORDINGTYPE']._serialized_start=3329 - _globals['_PAMRECORDINGTYPE']._serialized_end=3398 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=3236 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=3378 + _globals['_PAMOPERATIONTYPE']._serialized_start=3380 + _globals['_PAMOPERATIONTYPE']._serialized_end=3444 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=3446 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=3558 + _globals['_CONTROLLERMESSAGETYPE']._serialized_start=3560 + _globals['_CONTROLLERMESSAGETYPE']._serialized_end=3652 + _globals['_PAMRECORDINGTYPE']._serialized_start=3654 + _globals['_PAMRECORDINGTYPE']._serialized_end=3723 _globals['_PAMROTATIONSCHEDULE']._serialized_start=51 _globals['_PAMROTATIONSCHEDULE']._serialized_end=182 _globals['_PAMROTATIONSCHEDULESRESPONSE']._serialized_start=184 @@ -98,8 +90,10 @@ _globals['_PAMRECORDING']._serialized_end=2761 _globals['_PAMRECORDINGSRESPONSE']._serialized_start=2763 _globals['_PAMRECORDINGSRESPONSE']._serialized_end=2825 - _globals['_PAMLINK']._serialized_start=2827 - _globals['_PAMLINK']._serialized_end=2864 - _globals['_PAMDATA']._serialized_start=2866 - _globals['_PAMDATA']._serialized_end=2908 + _globals['_PAMDATA']._serialized_start=2827 + _globals['_PAMDATA']._serialized_end=2869 + _globals['_UIDLIST']._serialized_start=2871 + _globals['_UIDLIST']._serialized_end=2894 + _globals['_PAMRESOURCECONFIG']._serialized_start=2897 + _globals['_PAMRESOURCECONFIG']._serialized_end=3233 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi b/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi index 8d71561b..ff7dbed7 100644 --- a/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi @@ -351,14 +351,6 @@ class PAMRecordingsResponse(_message.Message): recordings: _containers.RepeatedCompositeFieldContainer[PAMRecording] def __init__(self, recordings: _Optional[_Iterable[_Union[PAMRecording, _Mapping]]] = ...) -> None: ... -class PAMLink(_message.Message): - __slots__ = ("head", "tail") - HEAD_FIELD_NUMBER: _ClassVar[int] - TAIL_FIELD_NUMBER: _ClassVar[int] - head: bytes - tail: bytes - def __init__(self, head: _Optional[bytes] = ..., tail: _Optional[bytes] = ...) -> None: ... - class PAMData(_message.Message): __slots__ = ("vertex", "content") VERTEX_FIELD_NUMBER: _ClassVar[int] @@ -366,3 +358,29 @@ class PAMData(_message.Message): vertex: bytes content: bytes def __init__(self, vertex: _Optional[bytes] = ..., content: _Optional[bytes] = ...) -> None: ... + +class UidList(_message.Message): + __slots__ = ("uids",) + UIDS_FIELD_NUMBER: _ClassVar[int] + uids: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, uids: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class PAMResourceConfig(_message.Message): + __slots__ = ("recordUid", "networkUid", "adminUid", "meta", "connectionSettings", "connectUsers", "domainUid", "jitSettings") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + NETWORKUID_FIELD_NUMBER: _ClassVar[int] + ADMINUID_FIELD_NUMBER: _ClassVar[int] + META_FIELD_NUMBER: _ClassVar[int] + CONNECTIONSETTINGS_FIELD_NUMBER: _ClassVar[int] + CONNECTUSERS_FIELD_NUMBER: _ClassVar[int] + DOMAINUID_FIELD_NUMBER: _ClassVar[int] + JITSETTINGS_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + networkUid: bytes + adminUid: bytes + meta: bytes + connectionSettings: bytes + connectUsers: UidList + domainUid: bytes + jitSettings: bytes + def __init__(self, recordUid: _Optional[bytes] = ..., networkUid: _Optional[bytes] = ..., adminUid: _Optional[bytes] = ..., meta: _Optional[bytes] = ..., connectionSettings: _Optional[bytes] = ..., connectUsers: _Optional[_Union[UidList, _Mapping]] = ..., domainUid: _Optional[bytes] = ..., jitSettings: _Optional[bytes] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/pedm_pb2.py b/keepersdk-package/src/keepersdk/proto/pedm_pb2.py index ac0b3d0d..c8177338 100644 --- a/keepersdk-package/src/keepersdk/proto/pedm_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/pedm_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: pedm.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'pedm.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -26,7 +18,7 @@ from . import NotificationCenter_pb2 as NotificationCenter__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\npedm.proto\x12\x04PEDM\x1a\x0c\x66older.proto\x1a\x18NotificationCenter.proto\"O\n\x17PEDMTOTPValidateRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\x12\x0c\n\x04\x63ode\x18\x03 \x01(\x05\";\n\nPedmStatus\x12\x0b\n\x03key\x18\x01 \x03(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x0f\n\x07message\x18\x03 \x01(\t\"\x89\x01\n\x12PedmStatusResponse\x12#\n\taddStatus\x18\x01 \x03(\x0b\x32\x10.PEDM.PedmStatus\x12&\n\x0cupdateStatus\x18\x02 \x03(\x0b\x32\x10.PEDM.PedmStatus\x12&\n\x0cremoveStatus\x18\x03 \x03(\x0b\x32\x10.PEDM.PedmStatus\"4\n\x0e\x44\x65ploymentData\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x65\x63PrivateKey\x18\x02 \x01(\x0c\"\x9a\x01\n\x17\x44\x65ploymentCreateRequest\x12\x15\n\rdeploymentUid\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61\x65sKey\x18\x02 \x01(\x0c\x12\x13\n\x0b\x65\x63PublicKey\x18\x03 \x01(\x0c\x12\x19\n\x11spiffeCertificate\x18\x04 \x01(\x0c\x12\x15\n\rencryptedData\x18\x05 \x01(\x0c\x12\x11\n\tagentData\x18\x06 \x01(\x0c\"\x8d\x01\n\x17\x44\x65ploymentUpdateRequest\x12\x15\n\rdeploymentUid\x18\x01 \x01(\x0c\x12\x15\n\rencryptedData\x18\x02 \x01(\x0c\x12)\n\x08\x64isabled\x18\x03 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x19\n\x11spiffeCertificate\x18\x04 \x01(\x0c\"\xa2\x01\n\x17ModifyDeploymentRequest\x12\x34\n\raddDeployment\x18\x01 \x03(\x0b\x32\x1d.PEDM.DeploymentCreateRequest\x12\x37\n\x10updateDeployment\x18\x02 \x03(\x0b\x32\x1d.PEDM.DeploymentUpdateRequest\x12\x18\n\x10removeDeployment\x18\x03 \x03(\x0c\"a\n\x0b\x41gentUpdate\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12)\n\x08\x64isabled\x18\x02 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x15\n\rdeploymentUid\x18\x03 \x01(\x0c\"Q\n\x12ModifyAgentRequest\x12&\n\x0bupdateAgent\x18\x02 \x03(\x0b\x32\x11.PEDM.AgentUpdate\x12\x13\n\x0bremoveAgent\x18\x03 \x03(\x0c\"^\n\tPolicyAdd\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x11\n\tplainData\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x14\n\x0c\x65ncryptedKey\x18\x04 \x01(\x0c\"K\n\x0cPolicyUpdate\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x11\n\tplainData\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\"s\n\rPolicyRequest\x12\"\n\taddPolicy\x18\x01 \x03(\x0b\x32\x0f.PEDM.PolicyAdd\x12(\n\x0cupdatePolicy\x18\x02 \x03(\x0b\x32\x12.PEDM.PolicyUpdate\x12\x14\n\x0cremovePolicy\x18\x03 \x03(\x0c\"6\n\nPolicyLink\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x15\n\rcollectionUid\x18\x02 \x03(\x0c\"E\n\x1aSetPolicyCollectionRequest\x12\'\n\rsetCollection\x18\x01 \x03(\x0b\x32\x10.PEDM.PolicyLink\"W\n\x0f\x43ollectionValue\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ollectionType\x18\x02 \x01(\x05\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\"z\n\x12\x43ollectionLinkData\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x0f\n\x07linkUid\x18\x02 \x01(\x0c\x12*\n\x08linkType\x18\x03 \x01(\x0e\x32\x18.PEDM.CollectionLinkType\x12\x10\n\x08linkData\x18\x04 \x01(\x0c\"\x8c\x01\n\x11\x43ollectionRequest\x12,\n\raddCollection\x18\x01 \x03(\x0b\x32\x15.PEDM.CollectionValue\x12/\n\x10updateCollection\x18\x02 \x03(\x0b\x32\x15.PEDM.CollectionValue\x12\x18\n\x10removeCollection\x18\x03 \x03(\x0c\"{\n\x18SetCollectionLinkRequest\x12/\n\raddCollection\x18\x01 \x03(\x0b\x32\x18.PEDM.CollectionLinkData\x12.\n\x10removeCollection\x18\x02 \x03(\x0b\x32\x14.PEDM.CollectionLink\"F\n\x15\x41pprovalActionRequest\x12\x0f\n\x07\x61pprove\x18\x01 \x03(\x0c\x12\x0c\n\x04\x64\x65ny\x18\x02 \x03(\x0c\x12\x0e\n\x06remove\x18\x03 \x03(\x0c\"\xab\x01\n\x0e\x44\x65ploymentNode\x12\x15\n\rdeploymentUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x02 \x01(\x08\x12\x0e\n\x06\x61\x65sKey\x18\x03 \x01(\x0c\x12\x13\n\x0b\x65\x63PublicKey\x18\x04 \x01(\x0c\x12\x15\n\rencryptedData\x18\x05 \x01(\x0c\x12\x11\n\tagentData\x18\x06 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x03\x12\x10\n\x08modified\x18\x08 \x01(\x03\"\xa8\x01\n\tAgentNode\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12\x11\n\tmachineId\x18\x02 \x01(\t\x12\x15\n\rdeploymentUid\x18\x03 \x01(\x0c\x12\x13\n\x0b\x65\x63PublicKey\x18\x04 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x05 \x01(\x08\x12\x15\n\rencryptedData\x18\x06 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x03\x12\x10\n\x08modified\x18\x08 \x01(\x03\"\x82\x01\n\nPolicyNode\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x11\n\tplainData\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x14\n\x0c\x65ncryptedKey\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\x10\n\x08modified\x18\x06 \x01(\x03\"g\n\x0e\x43ollectionNode\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ollectionType\x18\x02 \x01(\x05\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x04 \x01(\x03\"d\n\x0e\x43ollectionLink\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x0f\n\x07linkUid\x18\x02 \x01(\x0c\x12*\n\x08linkType\x18\x03 \x01(\x0e\x32\x18.PEDM.CollectionLinkType\"\x9d\x01\n\x12\x41pprovalStatusNode\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x46\n\x0e\x61pprovalStatus\x18\x02 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x10\n\x08modified\x18\n \x01(\x03\"\xb3\x01\n\x0c\x41pprovalNode\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x14\n\x0c\x61pprovalType\x18\x02 \x01(\x05\x12\x10\n\x08\x61gentUid\x18\x03 \x01(\x0c\x12\x13\n\x0b\x61\x63\x63ountInfo\x18\x04 \x01(\x0c\x12\x17\n\x0f\x61pplicationInfo\x18\x05 \x01(\x0c\x12\x15\n\rjustification\x18\x06 \x01(\x0c\x12\x10\n\x08\x65xpireIn\x18\x07 \x01(\x05\x12\x0f\n\x07\x63reated\x18\n \x01(\x03\"C\n\rFullSyncToken\x12\x15\n\rstartRevision\x18\x01 \x01(\x03\x12\x0e\n\x06\x65ntity\x18\x02 \x01(\x05\x12\x0b\n\x03key\x18\x03 \x03(\x0c\"$\n\x0cIncSyncToken\x12\x14\n\x0clastRevision\x18\x02 \x01(\x03\"h\n\rPedmSyncToken\x12\'\n\x08\x66ullSync\x18\x02 \x01(\x0b\x32\x13.PEDM.FullSyncTokenH\x00\x12%\n\x07incSync\x18\x03 \x01(\x0b\x32\x12.PEDM.IncSyncTokenH\x00\x42\x07\n\x05token\"/\n\x12GetPedmDataRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"\xad\x04\n\x13GetPedmDataResponse\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x12\n\nresetCache\x18\x02 \x01(\x08\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\x12\x1a\n\x12removedDeployments\x18\n \x03(\x0c\x12\x15\n\rremovedAgents\x18\x0b \x03(\x0c\x12\x17\n\x0fremovedPolicies\x18\x0c \x03(\x0c\x12\x19\n\x11removedCollection\x18\r \x03(\x0c\x12\x33\n\x15removedCollectionLink\x18\x0e \x03(\x0b\x32\x14.PEDM.CollectionLink\x12\x18\n\x10removedApprovals\x18\x0f \x03(\x0c\x12)\n\x0b\x64\x65ployments\x18\x14 \x03(\x0b\x32\x14.PEDM.DeploymentNode\x12\x1f\n\x06\x61gents\x18\x15 \x03(\x0b\x32\x0f.PEDM.AgentNode\x12\"\n\x08policies\x18\x16 \x03(\x0b\x32\x10.PEDM.PolicyNode\x12)\n\x0b\x63ollections\x18\x17 \x03(\x0b\x32\x14.PEDM.CollectionNode\x12,\n\x0e\x63ollectionLink\x18\x18 \x03(\x0b\x32\x14.PEDM.CollectionLink\x12%\n\tapprovals\x18\x19 \x03(\x0b\x32\x12.PEDM.ApprovalNode\x12\x30\n\x0e\x61pprovalStatus\x18\x1a \x03(\x0b\x32\x18.PEDM.ApprovalStatusNode\"]\n\x16\x41uditCollectionRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x10\n\x08valueUid\x18\x02 \x03(\x0c\x12\x16\n\x0e\x63ollectionName\x18\x03 \x03(\t\"h\n\x14\x41uditCollectionValue\x12\x16\n\x0e\x63ollectionName\x18\x01 \x01(\t\x12\x10\n\x08valueUid\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x04 \x01(\x03\"q\n\x17\x41uditCollectionResponse\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.PEDM.AuditCollectionValue\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\x12\x19\n\x11\x63ontinuationToken\x18\x03 \x01(\x0c\"H\n\x18GetCollectionLinkRequest\x12,\n\x0e\x63ollectionLink\x18\x01 \x03(\x0b\x32\x14.PEDM.CollectionLink\"Q\n\x19GetCollectionLinkResponse\x12\x34\n\x12\x63ollectionLinkData\x18\x01 \x03(\x0b\x32\x18.PEDM.CollectionLinkData*j\n\x12\x43ollectionLinkType\x12\r\n\tCLT_OTHER\x10\x00\x12\r\n\tCLT_AGENT\x10\x01\x12\x0e\n\nCLT_POLICY\x10\x02\x12\x12\n\x0e\x43LT_COLLECTION\x10\x03\x12\x12\n\x0e\x43LT_DEPLOYMENT\x10\x04\x42 \n\x18\x63om.keepersecurity.protoB\x04PEDMb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\npedm.proto\x12\x04PEDM\x1a\x0c\x66older.proto\x1a\x18NotificationCenter.proto\"O\n\x17PEDMTOTPValidateRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\x12\x0c\n\x04\x63ode\x18\x03 \x01(\x05\";\n\nPedmStatus\x12\x0b\n\x03key\x18\x01 \x03(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x0f\n\x07message\x18\x03 \x01(\t\"\x89\x01\n\x12PedmStatusResponse\x12#\n\taddStatus\x18\x01 \x03(\x0b\x32\x10.PEDM.PedmStatus\x12&\n\x0cupdateStatus\x18\x02 \x03(\x0b\x32\x10.PEDM.PedmStatus\x12&\n\x0cremoveStatus\x18\x03 \x03(\x0b\x32\x10.PEDM.PedmStatus\"4\n\x0e\x44\x65ploymentData\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x65\x63PrivateKey\x18\x02 \x01(\x0c\"\x9a\x01\n\x17\x44\x65ploymentCreateRequest\x12\x15\n\rdeploymentUid\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61\x65sKey\x18\x02 \x01(\x0c\x12\x13\n\x0b\x65\x63PublicKey\x18\x03 \x01(\x0c\x12\x19\n\x11spiffeCertificate\x18\x04 \x01(\x0c\x12\x15\n\rencryptedData\x18\x05 \x01(\x0c\x12\x11\n\tagentData\x18\x06 \x01(\x0c\"\x8d\x01\n\x17\x44\x65ploymentUpdateRequest\x12\x15\n\rdeploymentUid\x18\x01 \x01(\x0c\x12\x15\n\rencryptedData\x18\x02 \x01(\x0c\x12)\n\x08\x64isabled\x18\x03 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x19\n\x11spiffeCertificate\x18\x04 \x01(\x0c\"\xa2\x01\n\x17ModifyDeploymentRequest\x12\x34\n\raddDeployment\x18\x01 \x03(\x0b\x32\x1d.PEDM.DeploymentCreateRequest\x12\x37\n\x10updateDeployment\x18\x02 \x03(\x0b\x32\x1d.PEDM.DeploymentUpdateRequest\x12\x18\n\x10removeDeployment\x18\x03 \x03(\x0c\"a\n\x0b\x41gentUpdate\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12)\n\x08\x64isabled\x18\x02 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x15\n\rdeploymentUid\x18\x03 \x01(\x0c\"Q\n\x12ModifyAgentRequest\x12&\n\x0bupdateAgent\x18\x02 \x03(\x0b\x32\x11.PEDM.AgentUpdate\x12\x13\n\x0bremoveAgent\x18\x03 \x03(\x0c\"^\n\tPolicyAdd\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x11\n\tplainData\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x14\n\x0c\x65ncryptedKey\x18\x04 \x01(\x0c\"K\n\x0cPolicyUpdate\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x11\n\tplainData\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\"s\n\rPolicyRequest\x12\"\n\taddPolicy\x18\x01 \x03(\x0b\x32\x0f.PEDM.PolicyAdd\x12(\n\x0cupdatePolicy\x18\x02 \x03(\x0b\x32\x12.PEDM.PolicyUpdate\x12\x14\n\x0cremovePolicy\x18\x03 \x03(\x0c\"6\n\nPolicyLink\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x15\n\rcollectionUid\x18\x02 \x03(\x0c\"E\n\x1aSetPolicyCollectionRequest\x12\'\n\rsetCollection\x18\x01 \x03(\x0b\x32\x10.PEDM.PolicyLink\"L\n\x1bSetPolicyCollectionResponse\x12-\n\x13setCollectionStatus\x18\x01 \x03(\x0b\x32\x10.PEDM.PedmStatus\"W\n\x0f\x43ollectionValue\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ollectionType\x18\x02 \x01(\x05\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\"z\n\x12\x43ollectionLinkData\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x0f\n\x07linkUid\x18\x02 \x01(\x0c\x12*\n\x08linkType\x18\x03 \x01(\x0e\x32\x18.PEDM.CollectionLinkType\x12\x10\n\x08linkData\x18\x04 \x01(\x0c\"\x8c\x01\n\x11\x43ollectionRequest\x12,\n\raddCollection\x18\x01 \x03(\x0b\x32\x15.PEDM.CollectionValue\x12/\n\x10updateCollection\x18\x02 \x03(\x0b\x32\x15.PEDM.CollectionValue\x12\x18\n\x10removeCollection\x18\x03 \x03(\x0c\"{\n\x18SetCollectionLinkRequest\x12/\n\raddCollection\x18\x01 \x03(\x0b\x32\x18.PEDM.CollectionLinkData\x12.\n\x10removeCollection\x18\x02 \x03(\x0b\x32\x14.PEDM.CollectionLink\"F\n\x15\x41pprovalActionRequest\x12\x0f\n\x07\x61pprove\x18\x01 \x03(\x0c\x12\x0c\n\x04\x64\x65ny\x18\x02 \x03(\x0c\x12\x0e\n\x06remove\x18\x03 \x03(\x0c\"}\n\x16\x41pprovalActionResponse\x12!\n\x07\x61pprove\x18\x01 \x03(\x0b\x32\x10.PEDM.PedmStatus\x12\x1e\n\x04\x64\x65ny\x18\x02 \x03(\x0b\x32\x10.PEDM.PedmStatus\x12 \n\x06remove\x18\x03 \x03(\x0b\x32\x10.PEDM.PedmStatus\"\xab\x01\n\x0e\x44\x65ploymentNode\x12\x15\n\rdeploymentUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x02 \x01(\x08\x12\x0e\n\x06\x61\x65sKey\x18\x03 \x01(\x0c\x12\x13\n\x0b\x65\x63PublicKey\x18\x04 \x01(\x0c\x12\x15\n\rencryptedData\x18\x05 \x01(\x0c\x12\x11\n\tagentData\x18\x06 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x03\x12\x10\n\x08modified\x18\x08 \x01(\x03\"\xa8\x01\n\tAgentNode\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12\x11\n\tmachineId\x18\x02 \x01(\t\x12\x15\n\rdeploymentUid\x18\x03 \x01(\x0c\x12\x13\n\x0b\x65\x63PublicKey\x18\x04 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x05 \x01(\x08\x12\x15\n\rencryptedData\x18\x06 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x03\x12\x10\n\x08modified\x18\x08 \x01(\x03\"\x82\x01\n\nPolicyNode\x12\x11\n\tpolicyUid\x18\x01 \x01(\x0c\x12\x11\n\tplainData\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x14\n\x0c\x65ncryptedKey\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\x10\n\x08modified\x18\x06 \x01(\x03\"g\n\x0e\x43ollectionNode\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ollectionType\x18\x02 \x01(\x05\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x04 \x01(\x03\"d\n\x0e\x43ollectionLink\x12\x15\n\rcollectionUid\x18\x01 \x01(\x0c\x12\x0f\n\x07linkUid\x18\x02 \x01(\x0c\x12*\n\x08linkType\x18\x03 \x01(\x0e\x32\x18.PEDM.CollectionLinkType\"\x9d\x01\n\x12\x41pprovalStatusNode\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x46\n\x0e\x61pprovalStatus\x18\x02 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\x12\x18\n\x10\x65nterpriseUserId\x18\x03 \x01(\x03\x12\x10\n\x08modified\x18\n \x01(\x03\"\xb3\x01\n\x0c\x41pprovalNode\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x14\n\x0c\x61pprovalType\x18\x02 \x01(\x05\x12\x10\n\x08\x61gentUid\x18\x03 \x01(\x0c\x12\x13\n\x0b\x61\x63\x63ountInfo\x18\x04 \x01(\x0c\x12\x17\n\x0f\x61pplicationInfo\x18\x05 \x01(\x0c\x12\x15\n\rjustification\x18\x06 \x01(\x0c\x12\x10\n\x08\x65xpireIn\x18\x07 \x01(\x05\x12\x0f\n\x07\x63reated\x18\n \x01(\x03\"C\n\rFullSyncToken\x12\x15\n\rstartRevision\x18\x01 \x01(\x03\x12\x0e\n\x06\x65ntity\x18\x02 \x01(\x05\x12\x0b\n\x03key\x18\x03 \x03(\x0c\"$\n\x0cIncSyncToken\x12\x14\n\x0clastRevision\x18\x02 \x01(\x03\"h\n\rPedmSyncToken\x12\'\n\x08\x66ullSync\x18\x02 \x01(\x0b\x32\x13.PEDM.FullSyncTokenH\x00\x12%\n\x07incSync\x18\x03 \x01(\x0b\x32\x12.PEDM.IncSyncTokenH\x00\x42\x07\n\x05token\"/\n\x12GetPedmDataRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\"\xad\x04\n\x13GetPedmDataResponse\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x12\n\nresetCache\x18\x02 \x01(\x08\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\x12\x1a\n\x12removedDeployments\x18\n \x03(\x0c\x12\x15\n\rremovedAgents\x18\x0b \x03(\x0c\x12\x17\n\x0fremovedPolicies\x18\x0c \x03(\x0c\x12\x19\n\x11removedCollection\x18\r \x03(\x0c\x12\x33\n\x15removedCollectionLink\x18\x0e \x03(\x0b\x32\x14.PEDM.CollectionLink\x12\x18\n\x10removedApprovals\x18\x0f \x03(\x0c\x12)\n\x0b\x64\x65ployments\x18\x14 \x03(\x0b\x32\x14.PEDM.DeploymentNode\x12\x1f\n\x06\x61gents\x18\x15 \x03(\x0b\x32\x0f.PEDM.AgentNode\x12\"\n\x08policies\x18\x16 \x03(\x0b\x32\x10.PEDM.PolicyNode\x12)\n\x0b\x63ollections\x18\x17 \x03(\x0b\x32\x14.PEDM.CollectionNode\x12,\n\x0e\x63ollectionLink\x18\x18 \x03(\x0b\x32\x14.PEDM.CollectionLink\x12%\n\tapprovals\x18\x19 \x03(\x0b\x32\x12.PEDM.ApprovalNode\x12\x30\n\x0e\x61pprovalStatus\x18\x1a \x03(\x0b\x32\x18.PEDM.ApprovalStatusNode\"]\n\x16\x41uditCollectionRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x10\n\x08valueUid\x18\x02 \x03(\x0c\x12\x16\n\x0e\x63ollectionName\x18\x03 \x03(\t\"h\n\x14\x41uditCollectionValue\x12\x16\n\x0e\x63ollectionName\x18\x01 \x01(\t\x12\x10\n\x08valueUid\x18\x02 \x01(\x0c\x12\x15\n\rencryptedData\x18\x03 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x04 \x01(\x03\"q\n\x17\x41uditCollectionResponse\x12*\n\x06values\x18\x01 \x03(\x0b\x32\x1a.PEDM.AuditCollectionValue\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\x12\x19\n\x11\x63ontinuationToken\x18\x03 \x01(\x0c*j\n\x12\x43ollectionLinkType\x12\r\n\tCLT_OTHER\x10\x00\x12\r\n\tCLT_AGENT\x10\x01\x12\x0e\n\nCLT_POLICY\x10\x02\x12\x12\n\x0e\x43LT_COLLECTION\x10\x03\x12\x12\n\x0e\x43LT_DEPLOYMENT\x10\x04\x42 \n\x18\x63om.keepersecurity.protoB\x04PEDMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,8 +26,8 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\004PEDM' - _globals['_COLLECTIONLINKTYPE']._serialized_start=4336 - _globals['_COLLECTIONLINKTYPE']._serialized_end=4442 + _globals['_COLLECTIONLINKTYPE']._serialized_start=4384 + _globals['_COLLECTIONLINKTYPE']._serialized_end=4490 _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=60 _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=139 _globals['_PEDMSTATUS']._serialized_start=141 @@ -64,48 +56,48 @@ _globals['_POLICYLINK']._serialized_end=1388 _globals['_SETPOLICYCOLLECTIONREQUEST']._serialized_start=1390 _globals['_SETPOLICYCOLLECTIONREQUEST']._serialized_end=1459 - _globals['_COLLECTIONVALUE']._serialized_start=1461 - _globals['_COLLECTIONVALUE']._serialized_end=1548 - _globals['_COLLECTIONLINKDATA']._serialized_start=1550 - _globals['_COLLECTIONLINKDATA']._serialized_end=1672 - _globals['_COLLECTIONREQUEST']._serialized_start=1675 - _globals['_COLLECTIONREQUEST']._serialized_end=1815 - _globals['_SETCOLLECTIONLINKREQUEST']._serialized_start=1817 - _globals['_SETCOLLECTIONLINKREQUEST']._serialized_end=1940 - _globals['_APPROVALACTIONREQUEST']._serialized_start=1942 - _globals['_APPROVALACTIONREQUEST']._serialized_end=2012 - _globals['_DEPLOYMENTNODE']._serialized_start=2015 - _globals['_DEPLOYMENTNODE']._serialized_end=2186 - _globals['_AGENTNODE']._serialized_start=2189 - _globals['_AGENTNODE']._serialized_end=2357 - _globals['_POLICYNODE']._serialized_start=2360 - _globals['_POLICYNODE']._serialized_end=2490 - _globals['_COLLECTIONNODE']._serialized_start=2492 - _globals['_COLLECTIONNODE']._serialized_end=2595 - _globals['_COLLECTIONLINK']._serialized_start=2597 - _globals['_COLLECTIONLINK']._serialized_end=2697 - _globals['_APPROVALSTATUSNODE']._serialized_start=2700 - _globals['_APPROVALSTATUSNODE']._serialized_end=2857 - _globals['_APPROVALNODE']._serialized_start=2860 - _globals['_APPROVALNODE']._serialized_end=3039 - _globals['_FULLSYNCTOKEN']._serialized_start=3041 - _globals['_FULLSYNCTOKEN']._serialized_end=3108 - _globals['_INCSYNCTOKEN']._serialized_start=3110 - _globals['_INCSYNCTOKEN']._serialized_end=3146 - _globals['_PEDMSYNCTOKEN']._serialized_start=3148 - _globals['_PEDMSYNCTOKEN']._serialized_end=3252 - _globals['_GETPEDMDATAREQUEST']._serialized_start=3254 - _globals['_GETPEDMDATAREQUEST']._serialized_end=3301 - _globals['_GETPEDMDATARESPONSE']._serialized_start=3304 - _globals['_GETPEDMDATARESPONSE']._serialized_end=3861 - _globals['_AUDITCOLLECTIONREQUEST']._serialized_start=3863 - _globals['_AUDITCOLLECTIONREQUEST']._serialized_end=3956 - _globals['_AUDITCOLLECTIONVALUE']._serialized_start=3958 - _globals['_AUDITCOLLECTIONVALUE']._serialized_end=4062 - _globals['_AUDITCOLLECTIONRESPONSE']._serialized_start=4064 - _globals['_AUDITCOLLECTIONRESPONSE']._serialized_end=4177 - _globals['_GETCOLLECTIONLINKREQUEST']._serialized_start=4179 - _globals['_GETCOLLECTIONLINKREQUEST']._serialized_end=4251 - _globals['_GETCOLLECTIONLINKRESPONSE']._serialized_start=4253 - _globals['_GETCOLLECTIONLINKRESPONSE']._serialized_end=4334 + _globals['_SETPOLICYCOLLECTIONRESPONSE']._serialized_start=1461 + _globals['_SETPOLICYCOLLECTIONRESPONSE']._serialized_end=1537 + _globals['_COLLECTIONVALUE']._serialized_start=1539 + _globals['_COLLECTIONVALUE']._serialized_end=1626 + _globals['_COLLECTIONLINKDATA']._serialized_start=1628 + _globals['_COLLECTIONLINKDATA']._serialized_end=1750 + _globals['_COLLECTIONREQUEST']._serialized_start=1753 + _globals['_COLLECTIONREQUEST']._serialized_end=1893 + _globals['_SETCOLLECTIONLINKREQUEST']._serialized_start=1895 + _globals['_SETCOLLECTIONLINKREQUEST']._serialized_end=2018 + _globals['_APPROVALACTIONREQUEST']._serialized_start=2020 + _globals['_APPROVALACTIONREQUEST']._serialized_end=2090 + _globals['_APPROVALACTIONRESPONSE']._serialized_start=2092 + _globals['_APPROVALACTIONRESPONSE']._serialized_end=2217 + _globals['_DEPLOYMENTNODE']._serialized_start=2220 + _globals['_DEPLOYMENTNODE']._serialized_end=2391 + _globals['_AGENTNODE']._serialized_start=2394 + _globals['_AGENTNODE']._serialized_end=2562 + _globals['_POLICYNODE']._serialized_start=2565 + _globals['_POLICYNODE']._serialized_end=2695 + _globals['_COLLECTIONNODE']._serialized_start=2697 + _globals['_COLLECTIONNODE']._serialized_end=2800 + _globals['_COLLECTIONLINK']._serialized_start=2802 + _globals['_COLLECTIONLINK']._serialized_end=2902 + _globals['_APPROVALSTATUSNODE']._serialized_start=2905 + _globals['_APPROVALSTATUSNODE']._serialized_end=3062 + _globals['_APPROVALNODE']._serialized_start=3065 + _globals['_APPROVALNODE']._serialized_end=3244 + _globals['_FULLSYNCTOKEN']._serialized_start=3246 + _globals['_FULLSYNCTOKEN']._serialized_end=3313 + _globals['_INCSYNCTOKEN']._serialized_start=3315 + _globals['_INCSYNCTOKEN']._serialized_end=3351 + _globals['_PEDMSYNCTOKEN']._serialized_start=3353 + _globals['_PEDMSYNCTOKEN']._serialized_end=3457 + _globals['_GETPEDMDATAREQUEST']._serialized_start=3459 + _globals['_GETPEDMDATAREQUEST']._serialized_end=3506 + _globals['_GETPEDMDATARESPONSE']._serialized_start=3509 + _globals['_GETPEDMDATARESPONSE']._serialized_end=4066 + _globals['_AUDITCOLLECTIONREQUEST']._serialized_start=4068 + _globals['_AUDITCOLLECTIONREQUEST']._serialized_end=4161 + _globals['_AUDITCOLLECTIONVALUE']._serialized_start=4163 + _globals['_AUDITCOLLECTIONVALUE']._serialized_end=4267 + _globals['_AUDITCOLLECTIONRESPONSE']._serialized_start=4269 + _globals['_AUDITCOLLECTIONRESPONSE']._serialized_end=4382 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi b/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi index ce61d348..7aeeac6e 100644 --- a/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi @@ -161,6 +161,12 @@ class SetPolicyCollectionRequest(_message.Message): setCollection: _containers.RepeatedCompositeFieldContainer[PolicyLink] def __init__(self, setCollection: _Optional[_Iterable[_Union[PolicyLink, _Mapping]]] = ...) -> None: ... +class SetPolicyCollectionResponse(_message.Message): + __slots__ = ("setCollectionStatus",) + SETCOLLECTIONSTATUS_FIELD_NUMBER: _ClassVar[int] + setCollectionStatus: _containers.RepeatedCompositeFieldContainer[PedmStatus] + def __init__(self, setCollectionStatus: _Optional[_Iterable[_Union[PedmStatus, _Mapping]]] = ...) -> None: ... + class CollectionValue(_message.Message): __slots__ = ("collectionUid", "collectionType", "encryptedData") COLLECTIONUID_FIELD_NUMBER: _ClassVar[int] @@ -211,6 +217,16 @@ class ApprovalActionRequest(_message.Message): remove: _containers.RepeatedScalarFieldContainer[bytes] def __init__(self, approve: _Optional[_Iterable[bytes]] = ..., deny: _Optional[_Iterable[bytes]] = ..., remove: _Optional[_Iterable[bytes]] = ...) -> None: ... +class ApprovalActionResponse(_message.Message): + __slots__ = ("approve", "deny", "remove") + APPROVE_FIELD_NUMBER: _ClassVar[int] + DENY_FIELD_NUMBER: _ClassVar[int] + REMOVE_FIELD_NUMBER: _ClassVar[int] + approve: _containers.RepeatedCompositeFieldContainer[PedmStatus] + deny: _containers.RepeatedCompositeFieldContainer[PedmStatus] + remove: _containers.RepeatedCompositeFieldContainer[PedmStatus] + def __init__(self, approve: _Optional[_Iterable[_Union[PedmStatus, _Mapping]]] = ..., deny: _Optional[_Iterable[_Union[PedmStatus, _Mapping]]] = ..., remove: _Optional[_Iterable[_Union[PedmStatus, _Mapping]]] = ...) -> None: ... + class DeploymentNode(_message.Message): __slots__ = ("deploymentUid", "disabled", "aesKey", "ecPublicKey", "encryptedData", "agentData", "created", "modified") DEPLOYMENTUID_FIELD_NUMBER: _ClassVar[int] @@ -418,15 +434,3 @@ class AuditCollectionResponse(_message.Message): hasMore: bool continuationToken: bytes def __init__(self, values: _Optional[_Iterable[_Union[AuditCollectionValue, _Mapping]]] = ..., hasMore: bool = ..., continuationToken: _Optional[bytes] = ...) -> None: ... - -class GetCollectionLinkRequest(_message.Message): - __slots__ = ("collectionLink",) - COLLECTIONLINK_FIELD_NUMBER: _ClassVar[int] - collectionLink: _containers.RepeatedCompositeFieldContainer[CollectionLink] - def __init__(self, collectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ...) -> None: ... - -class GetCollectionLinkResponse(_message.Message): - __slots__ = ("collectionLinkData",) - COLLECTIONLINKDATA_FIELD_NUMBER: _ClassVar[int] - collectionLinkData: _containers.RepeatedCompositeFieldContainer[CollectionLinkData] - def __init__(self, collectionLinkData: _Optional[_Iterable[_Union[CollectionLinkData, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/push_pb2.py b/keepersdk-package/src/keepersdk/proto/push_pb2.py index 4f624185..47538aa1 100644 --- a/keepersdk-package/src/keepersdk/proto/push_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/push_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: push.proto -# Protobuf Python Version: 5.28.3 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 3, - '', - 'push.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/record_pb2.py b/keepersdk-package/src/keepersdk/proto/record_pb2.py index 9ea7d106..d908a34c 100644 --- a/keepersdk-package/src/keepersdk/proto/record_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/record_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: record.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'record.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/router_pb2.py b/keepersdk-package/src/keepersdk/proto/router_pb2.py index e827e027..2b1c2837 100644 --- a/keepersdk-package/src/keepersdk/proto/router_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/router_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: router.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'router.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -25,7 +17,7 @@ from . import pam_pb2 as pam__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crouter.proto\x12\x06Router\x1a\tpam.proto\"r\n\x0eRouterResponse\x12\x30\n\x0cresponseCode\x18\x01 \x01(\x0e\x32\x1a.Router.RouterResponseCode\x12\x14\n\x0c\x65rrorMessage\x18\x02 \x01(\t\x12\x18\n\x10\x65ncryptedPayload\x18\x03 \x01(\x0c\"\xaf\x01\n\x17RouterControllerMessage\x12/\n\x0bmessageType\x18\x01 \x01(\x0e\x32\x1a.PAM.ControllerMessageType\x12\x12\n\nmessageUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x16\n\x0estreamResponse\x18\x04 \x01(\x08\x12\x0f\n\x07payload\x18\x05 \x01(\x0c\x12\x0f\n\x07timeout\x18\x06 \x01(\x05\"\xec\x01\n\x0eRouterUserAuth\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\x0e\n\x06userId\x18\x03 \x01(\x05\x12\x18\n\x10\x65nterpriseUserId\x18\x04 \x01(\x03\x12\x12\n\ndeviceName\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x06 \x01(\x0c\x12\x17\n\x0f\x63lientVersionId\x18\x07 \x01(\x05\x12\x14\n\x0cneedUsername\x18\x08 \x01(\x08\x12\x10\n\x08username\x18\t \x01(\t\x12\x17\n\x0fmspEnterpriseId\x18\n \x01(\x05\"\x83\x02\n\x10RouterDeviceAuth\x12\x10\n\x08\x63lientId\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x14\n\x0c\x65nterpriseId\x18\x04 \x01(\x05\x12\x0e\n\x06nodeId\x18\x05 \x01(\x03\x12\x12\n\ndeviceName\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x07 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x08 \x01(\t\x12\x15\n\rcontrollerUid\x18\t \x01(\x0c\x12\x11\n\townerUser\x18\n \x01(\t\x12\x11\n\tchallenge\x18\x0b \x01(\t\x12\x0f\n\x07ownerId\x18\x0c \x01(\x05\"\x83\x01\n\x14RouterRecordRotation\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x13\n\x0bresourceUid\x18\x04 \x01(\x0c\x12\x12\n\nnoSchedule\x18\x05 \x01(\x08\"E\n\x1cRouterRecordRotationsRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x0f\n\x07records\x18\x02 \x03(\x0c\"a\n\x1dRouterRecordRotationsResponse\x12/\n\trotations\x18\x01 \x03(\x0b\x32\x1c.Router.RouterRecordRotation\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\"\xed\x01\n\x12RouterRotationInfo\x12,\n\x06status\x18\x01 \x01(\x0e\x32\x1c.Router.RouterRotationStatus\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x13\n\x0bresourceUid\x18\x03 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x04 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x05 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x06 \x01(\t\x12\x12\n\nscriptName\x18\x07 \x01(\t\x12\x15\n\rpwdComplexity\x18\x08 \x01(\t\x12\x10\n\x08\x64isabled\x18\t \x01(\x08\"\x84\x02\n\x1bRouterRecordRotationRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x03 \x01(\x0c\x12\x13\n\x0bresourceUid\x18\x04 \x01(\x0c\x12\x10\n\x08schedule\x18\x05 \x01(\t\x12\x18\n\x10\x65nterpriseUserId\x18\x06 \x01(\x03\x12\x15\n\rpwdComplexity\x18\x07 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x08 \x01(\x08\x12\x15\n\rremoteAddress\x18\t \x01(\t\x12\x17\n\x0f\x63lientVersionId\x18\n \x01(\x05\x12\x0c\n\x04noop\x18\x0b \x01(\x08\"<\n\x17UserRecordAccessRequest\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\"a\n\x18UserRecordAccessResponse\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x32\n\x0b\x61\x63\x63\x65ssLevel\x18\x02 \x01(\x0e\x32\x1d.Router.UserRecordAccessLevel\"8\n\x10RotationSchedule\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x10\n\x08schedule\x18\x02 \x01(\t\"\x90\x01\n\x12\x41piCallbackRequest\x12\x13\n\x0bresourceUid\x18\x01 \x01(\x0c\x12.\n\tschedules\x18\x02 \x03(\x0b\x32\x1b.Router.ApiCallbackSchedule\x12\x0b\n\x03url\x18\x03 \x01(\t\x12(\n\x0bserviceType\x18\x04 \x01(\x0e\x32\x13.Router.ServiceType\"5\n\x13\x41piCallbackSchedule\x12\x10\n\x08schedule\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"@\n\x16RouterScheduledActions\x12\x10\n\x08schedule\x18\x01 \x01(\t\x12\x14\n\x0cresourceUids\x18\x02 \x03(\x0c\"Y\n\x1cRouterRecordsRotationRequest\x12\x39\n\x11rotationSchedules\x18\x01 \x03(\x0b\x32\x1e.Router.RouterScheduledActions\"\x85\x01\n\x14\x43onnectionParameters\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0e\n\x06userId\x18\x03 \x01(\x05\x12\x15\n\rcontrollerUid\x18\x04 \x01(\x0c\x12\x1c\n\x14\x63redentialsRecordUid\x18\x05 \x01(\x0c\"O\n\x1aValidateConnectionsRequest\x12\x31\n\x0b\x63onnections\x18\x01 \x03(\x0b\x32\x1c.Router.ConnectionParameters\"J\n\x1b\x43onnectionValidationFailure\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\x14\n\x0c\x65rrorMessage\x18\x02 \x01(\t\"]\n\x1bValidateConnectionsResponse\x12>\n\x11\x66\x61iledConnections\x18\x01 \x03(\x0b\x32#.Router.ConnectionValidationFailure\"1\n\x15GetEnforcementRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\";\n\x0f\x45nforcementType\x12\x19\n\x11\x65nforcementTypeId\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t\"K\n\x16GetEnforcementResponse\x12\x31\n\x10\x65nforcementTypes\x18\x01 \x03(\x0b\x32\x17.Router.EnforcementType\"O\n\x17PEDMTOTPValidateRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\x12\x0c\n\x04\x63ode\x18\x03 \x01(\x05*\x98\x02\n\x12RouterResponseCode\x12\n\n\x06RRC_OK\x10\x00\x12\x15\n\x11RRC_GENERAL_ERROR\x10\x01\x12\x13\n\x0fRRC_NOT_ALLOWED\x10\x02\x12\x13\n\x0fRRC_BAD_REQUEST\x10\x03\x12\x0f\n\x0bRRC_TIMEOUT\x10\x04\x12\x11\n\rRRC_BAD_STATE\x10\x05\x12\x17\n\x13RRC_CONTROLLER_DOWN\x10\x06\x12\x16\n\x12RRC_WRONG_INSTANCE\x10\x07\x12+\n\'RRC_NOT_ALLOWED_ENFORCEMENT_NOT_ENABLED\x10\x08\x12\x33\n/RRC_NOT_ALLOWED_PAM_CONFIG_FEATURES_NOT_ENABLED\x10\t*k\n\x14RouterRotationStatus\x12\x0e\n\nRRS_ONLINE\x10\x00\x12\x13\n\x0fRRS_NO_ROTATION\x10\x01\x12\x15\n\x11RRS_NO_CONTROLLER\x10\x02\x12\x17\n\x13RRS_CONTROLLER_DOWN\x10\x03*}\n\x15UserRecordAccessLevel\x12\r\n\tRRAL_NONE\x10\x00\x12\r\n\tRRAL_READ\x10\x01\x12\x0e\n\nRRAL_SHARE\x10\x02\x12\r\n\tRRAL_EDIT\x10\x03\x12\x17\n\x13RRAL_EDIT_AND_SHARE\x10\x04\x12\x0e\n\nRRAL_OWNER\x10\x05*.\n\x0bServiceType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x06\n\x02KA\x10\x01\x12\x06\n\x02\x42I\x10\x02\x42\"\n\x18\x63om.keepersecurity.protoB\x06Routerb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crouter.proto\x12\x06Router\x1a\tpam.proto\"r\n\x0eRouterResponse\x12\x30\n\x0cresponseCode\x18\x01 \x01(\x0e\x32\x1a.Router.RouterResponseCode\x12\x14\n\x0c\x65rrorMessage\x18\x02 \x01(\t\x12\x18\n\x10\x65ncryptedPayload\x18\x03 \x01(\x0c\"\xaf\x01\n\x17RouterControllerMessage\x12/\n\x0bmessageType\x18\x01 \x01(\x0e\x32\x1a.PAM.ControllerMessageType\x12\x12\n\nmessageUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x16\n\x0estreamResponse\x18\x04 \x01(\x08\x12\x0f\n\x07payload\x18\x05 \x01(\x0c\x12\x0f\n\x07timeout\x18\x06 \x01(\x05\"\x99\x02\n\x0eRouterUserAuth\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\x0e\n\x06userId\x18\x03 \x01(\x05\x12\x18\n\x10\x65nterpriseUserId\x18\x04 \x01(\x03\x12\x12\n\ndeviceName\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x06 \x01(\x0c\x12\x17\n\x0f\x63lientVersionId\x18\x07 \x01(\x05\x12\x14\n\x0cneedUsername\x18\x08 \x01(\x08\x12\x10\n\x08username\x18\t \x01(\t\x12\x17\n\x0fmspEnterpriseId\x18\n \x01(\x05\x12\x13\n\x0bisPedmAdmin\x18\x0b \x01(\x08\x12\x16\n\x0emcEnterpriseId\x18\x0c \x01(\x05\"\x83\x02\n\x10RouterDeviceAuth\x12\x10\n\x08\x63lientId\x18\x01 \x01(\t\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x14\n\x0c\x65nterpriseId\x18\x04 \x01(\x05\x12\x0e\n\x06nodeId\x18\x05 \x01(\x03\x12\x12\n\ndeviceName\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x07 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x08 \x01(\t\x12\x15\n\rcontrollerUid\x18\t \x01(\x0c\x12\x11\n\townerUser\x18\n \x01(\t\x12\x11\n\tchallenge\x18\x0b \x01(\t\x12\x0f\n\x07ownerId\x18\x0c \x01(\x05\"\x83\x01\n\x14RouterRecordRotation\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x13\n\x0bresourceUid\x18\x04 \x01(\x0c\x12\x12\n\nnoSchedule\x18\x05 \x01(\x08\"E\n\x1cRouterRecordRotationsRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x0f\n\x07records\x18\x02 \x03(\x0c\"a\n\x1dRouterRecordRotationsResponse\x12/\n\trotations\x18\x01 \x03(\x0b\x32\x1c.Router.RouterRecordRotation\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\"\xed\x01\n\x12RouterRotationInfo\x12,\n\x06status\x18\x01 \x01(\x0e\x32\x1c.Router.RouterRotationStatus\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x13\n\x0bresourceUid\x18\x03 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x04 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x05 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x06 \x01(\t\x12\x12\n\nscriptName\x18\x07 \x01(\t\x12\x15\n\rpwdComplexity\x18\x08 \x01(\t\x12\x10\n\x08\x64isabled\x18\t \x01(\x08\"\x84\x02\n\x1bRouterRecordRotationRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x03 \x01(\x0c\x12\x13\n\x0bresourceUid\x18\x04 \x01(\x0c\x12\x10\n\x08schedule\x18\x05 \x01(\t\x12\x18\n\x10\x65nterpriseUserId\x18\x06 \x01(\x03\x12\x15\n\rpwdComplexity\x18\x07 \x01(\x0c\x12\x10\n\x08\x64isabled\x18\x08 \x01(\x08\x12\x15\n\rremoteAddress\x18\t \x01(\t\x12\x17\n\x0f\x63lientVersionId\x18\n \x01(\x05\x12\x0c\n\x04noop\x18\x0b \x01(\x08\"<\n\x17UserRecordAccessRequest\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\"a\n\x18UserRecordAccessResponse\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x32\n\x0b\x61\x63\x63\x65ssLevel\x18\x02 \x01(\x0e\x32\x1d.Router.UserRecordAccessLevel\"8\n\x10RotationSchedule\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x10\n\x08schedule\x18\x02 \x01(\t\"\x90\x01\n\x12\x41piCallbackRequest\x12\x13\n\x0bresourceUid\x18\x01 \x01(\x0c\x12.\n\tschedules\x18\x02 \x03(\x0b\x32\x1b.Router.ApiCallbackSchedule\x12\x0b\n\x03url\x18\x03 \x01(\t\x12(\n\x0bserviceType\x18\x04 \x01(\x0e\x32\x13.Router.ServiceType\"5\n\x13\x41piCallbackSchedule\x12\x10\n\x08schedule\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"@\n\x16RouterScheduledActions\x12\x10\n\x08schedule\x18\x01 \x01(\t\x12\x14\n\x0cresourceUids\x18\x02 \x03(\x0c\"Y\n\x1cRouterRecordsRotationRequest\x12\x39\n\x11rotationSchedules\x18\x01 \x03(\x0b\x32\x1e.Router.RouterScheduledActions\"\x85\x01\n\x14\x43onnectionParameters\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0e\n\x06userId\x18\x03 \x01(\x05\x12\x15\n\rcontrollerUid\x18\x04 \x01(\x0c\x12\x1c\n\x14\x63redentialsRecordUid\x18\x05 \x01(\x0c\"O\n\x1aValidateConnectionsRequest\x12\x31\n\x0b\x63onnections\x18\x01 \x03(\x0b\x32\x1c.Router.ConnectionParameters\"J\n\x1b\x43onnectionValidationFailure\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\x14\n\x0c\x65rrorMessage\x18\x02 \x01(\t\"]\n\x1bValidateConnectionsResponse\x12>\n\x11\x66\x61iledConnections\x18\x01 \x03(\x0b\x32#.Router.ConnectionValidationFailure\"1\n\x15GetEnforcementRequest\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\";\n\x0f\x45nforcementType\x12\x19\n\x11\x65nforcementTypeId\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t\"p\n\x16GetEnforcementResponse\x12\x31\n\x10\x65nforcementTypes\x18\x01 \x03(\x0b\x32\x17.Router.EnforcementType\x12\x10\n\x08\x61\x64\x64OnIds\x18\x02 \x03(\x05\x12\x11\n\tisInTrial\x18\x03 \x01(\x08\"O\n\x17PEDMTOTPValidateRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\x12\x0c\n\x04\x63ode\x18\x03 \x01(\x05*\x98\x02\n\x12RouterResponseCode\x12\n\n\x06RRC_OK\x10\x00\x12\x15\n\x11RRC_GENERAL_ERROR\x10\x01\x12\x13\n\x0fRRC_NOT_ALLOWED\x10\x02\x12\x13\n\x0fRRC_BAD_REQUEST\x10\x03\x12\x0f\n\x0bRRC_TIMEOUT\x10\x04\x12\x11\n\rRRC_BAD_STATE\x10\x05\x12\x17\n\x13RRC_CONTROLLER_DOWN\x10\x06\x12\x16\n\x12RRC_WRONG_INSTANCE\x10\x07\x12+\n\'RRC_NOT_ALLOWED_ENFORCEMENT_NOT_ENABLED\x10\x08\x12\x33\n/RRC_NOT_ALLOWED_PAM_CONFIG_FEATURES_NOT_ENABLED\x10\t*k\n\x14RouterRotationStatus\x12\x0e\n\nRRS_ONLINE\x10\x00\x12\x13\n\x0fRRS_NO_ROTATION\x10\x01\x12\x15\n\x11RRS_NO_CONTROLLER\x10\x02\x12\x17\n\x13RRS_CONTROLLER_DOWN\x10\x03*}\n\x15UserRecordAccessLevel\x12\r\n\tRRAL_NONE\x10\x00\x12\r\n\tRRAL_READ\x10\x01\x12\x0e\n\nRRAL_SHARE\x10\x02\x12\r\n\tRRAL_EDIT\x10\x03\x12\x17\n\x13RRAL_EDIT_AND_SHARE\x10\x04\x12\x0e\n\nRRAL_OWNER\x10\x05*.\n\x0bServiceType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x06\n\x02KA\x10\x01\x12\x06\n\x02\x42I\x10\x02\x42\"\n\x18\x63om.keepersecurity.protoB\x06Routerb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,60 +25,60 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\006Router' - _globals['_ROUTERRESPONSECODE']._serialized_start=2874 - _globals['_ROUTERRESPONSECODE']._serialized_end=3154 - _globals['_ROUTERROTATIONSTATUS']._serialized_start=3156 - _globals['_ROUTERROTATIONSTATUS']._serialized_end=3263 - _globals['_USERRECORDACCESSLEVEL']._serialized_start=3265 - _globals['_USERRECORDACCESSLEVEL']._serialized_end=3390 - _globals['_SERVICETYPE']._serialized_start=3392 - _globals['_SERVICETYPE']._serialized_end=3438 + _globals['_ROUTERRESPONSECODE']._serialized_start=2956 + _globals['_ROUTERRESPONSECODE']._serialized_end=3236 + _globals['_ROUTERROTATIONSTATUS']._serialized_start=3238 + _globals['_ROUTERROTATIONSTATUS']._serialized_end=3345 + _globals['_USERRECORDACCESSLEVEL']._serialized_start=3347 + _globals['_USERRECORDACCESSLEVEL']._serialized_end=3472 + _globals['_SERVICETYPE']._serialized_start=3474 + _globals['_SERVICETYPE']._serialized_end=3520 _globals['_ROUTERRESPONSE']._serialized_start=35 _globals['_ROUTERRESPONSE']._serialized_end=149 _globals['_ROUTERCONTROLLERMESSAGE']._serialized_start=152 _globals['_ROUTERCONTROLLERMESSAGE']._serialized_end=327 _globals['_ROUTERUSERAUTH']._serialized_start=330 - _globals['_ROUTERUSERAUTH']._serialized_end=566 - _globals['_ROUTERDEVICEAUTH']._serialized_start=569 - _globals['_ROUTERDEVICEAUTH']._serialized_end=828 - _globals['_ROUTERRECORDROTATION']._serialized_start=831 - _globals['_ROUTERRECORDROTATION']._serialized_end=962 - _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_start=964 - _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_end=1033 - _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_start=1035 - _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_end=1132 - _globals['_ROUTERROTATIONINFO']._serialized_start=1135 - _globals['_ROUTERROTATIONINFO']._serialized_end=1372 - _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_start=1375 - _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_end=1635 - _globals['_USERRECORDACCESSREQUEST']._serialized_start=1637 - _globals['_USERRECORDACCESSREQUEST']._serialized_end=1697 - _globals['_USERRECORDACCESSRESPONSE']._serialized_start=1699 - _globals['_USERRECORDACCESSRESPONSE']._serialized_end=1796 - _globals['_ROTATIONSCHEDULE']._serialized_start=1798 - _globals['_ROTATIONSCHEDULE']._serialized_end=1854 - _globals['_APICALLBACKREQUEST']._serialized_start=1857 - _globals['_APICALLBACKREQUEST']._serialized_end=2001 - _globals['_APICALLBACKSCHEDULE']._serialized_start=2003 - _globals['_APICALLBACKSCHEDULE']._serialized_end=2056 - _globals['_ROUTERSCHEDULEDACTIONS']._serialized_start=2058 - _globals['_ROUTERSCHEDULEDACTIONS']._serialized_end=2122 - _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_start=2124 - _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_end=2213 - _globals['_CONNECTIONPARAMETERS']._serialized_start=2216 - _globals['_CONNECTIONPARAMETERS']._serialized_end=2349 - _globals['_VALIDATECONNECTIONSREQUEST']._serialized_start=2351 - _globals['_VALIDATECONNECTIONSREQUEST']._serialized_end=2430 - _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_start=2432 - _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_end=2506 - _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_start=2508 - _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_end=2601 - _globals['_GETENFORCEMENTREQUEST']._serialized_start=2603 - _globals['_GETENFORCEMENTREQUEST']._serialized_end=2652 - _globals['_ENFORCEMENTTYPE']._serialized_start=2654 - _globals['_ENFORCEMENTTYPE']._serialized_end=2713 - _globals['_GETENFORCEMENTRESPONSE']._serialized_start=2715 - _globals['_GETENFORCEMENTRESPONSE']._serialized_end=2790 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=2792 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=2871 + _globals['_ROUTERUSERAUTH']._serialized_end=611 + _globals['_ROUTERDEVICEAUTH']._serialized_start=614 + _globals['_ROUTERDEVICEAUTH']._serialized_end=873 + _globals['_ROUTERRECORDROTATION']._serialized_start=876 + _globals['_ROUTERRECORDROTATION']._serialized_end=1007 + _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_start=1009 + _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_end=1078 + _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_start=1080 + _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_end=1177 + _globals['_ROUTERROTATIONINFO']._serialized_start=1180 + _globals['_ROUTERROTATIONINFO']._serialized_end=1417 + _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_start=1420 + _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_end=1680 + _globals['_USERRECORDACCESSREQUEST']._serialized_start=1682 + _globals['_USERRECORDACCESSREQUEST']._serialized_end=1742 + _globals['_USERRECORDACCESSRESPONSE']._serialized_start=1744 + _globals['_USERRECORDACCESSRESPONSE']._serialized_end=1841 + _globals['_ROTATIONSCHEDULE']._serialized_start=1843 + _globals['_ROTATIONSCHEDULE']._serialized_end=1899 + _globals['_APICALLBACKREQUEST']._serialized_start=1902 + _globals['_APICALLBACKREQUEST']._serialized_end=2046 + _globals['_APICALLBACKSCHEDULE']._serialized_start=2048 + _globals['_APICALLBACKSCHEDULE']._serialized_end=2101 + _globals['_ROUTERSCHEDULEDACTIONS']._serialized_start=2103 + _globals['_ROUTERSCHEDULEDACTIONS']._serialized_end=2167 + _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_start=2169 + _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_end=2258 + _globals['_CONNECTIONPARAMETERS']._serialized_start=2261 + _globals['_CONNECTIONPARAMETERS']._serialized_end=2394 + _globals['_VALIDATECONNECTIONSREQUEST']._serialized_start=2396 + _globals['_VALIDATECONNECTIONSREQUEST']._serialized_end=2475 + _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_start=2477 + _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_end=2551 + _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_start=2553 + _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_end=2646 + _globals['_GETENFORCEMENTREQUEST']._serialized_start=2648 + _globals['_GETENFORCEMENTREQUEST']._serialized_end=2697 + _globals['_ENFORCEMENTTYPE']._serialized_start=2699 + _globals['_ENFORCEMENTTYPE']._serialized_end=2758 + _globals['_GETENFORCEMENTRESPONSE']._serialized_start=2760 + _globals['_GETENFORCEMENTRESPONSE']._serialized_end=2872 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=2874 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=2953 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/router_pb2.pyi b/keepersdk-package/src/keepersdk/proto/router_pb2.pyi index 0a722f5d..b5e2d325 100644 --- a/keepersdk-package/src/keepersdk/proto/router_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/router_pb2.pyi @@ -92,7 +92,7 @@ class RouterControllerMessage(_message.Message): def __init__(self, messageType: _Optional[_Union[_pam_pb2.ControllerMessageType, str]] = ..., messageUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., streamResponse: bool = ..., payload: _Optional[bytes] = ..., timeout: _Optional[int] = ...) -> None: ... class RouterUserAuth(_message.Message): - __slots__ = ("transmissionKey", "sessionToken", "userId", "enterpriseUserId", "deviceName", "deviceToken", "clientVersionId", "needUsername", "username", "mspEnterpriseId") + __slots__ = ("transmissionKey", "sessionToken", "userId", "enterpriseUserId", "deviceName", "deviceToken", "clientVersionId", "needUsername", "username", "mspEnterpriseId", "isPedmAdmin", "mcEnterpriseId") TRANSMISSIONKEY_FIELD_NUMBER: _ClassVar[int] SESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] USERID_FIELD_NUMBER: _ClassVar[int] @@ -103,6 +103,8 @@ class RouterUserAuth(_message.Message): NEEDUSERNAME_FIELD_NUMBER: _ClassVar[int] USERNAME_FIELD_NUMBER: _ClassVar[int] MSPENTERPRISEID_FIELD_NUMBER: _ClassVar[int] + ISPEDMADMIN_FIELD_NUMBER: _ClassVar[int] + MCENTERPRISEID_FIELD_NUMBER: _ClassVar[int] transmissionKey: bytes sessionToken: bytes userId: int @@ -113,7 +115,9 @@ class RouterUserAuth(_message.Message): needUsername: bool username: str mspEnterpriseId: int - def __init__(self, transmissionKey: _Optional[bytes] = ..., sessionToken: _Optional[bytes] = ..., userId: _Optional[int] = ..., enterpriseUserId: _Optional[int] = ..., deviceName: _Optional[str] = ..., deviceToken: _Optional[bytes] = ..., clientVersionId: _Optional[int] = ..., needUsername: bool = ..., username: _Optional[str] = ..., mspEnterpriseId: _Optional[int] = ...) -> None: ... + isPedmAdmin: bool + mcEnterpriseId: int + def __init__(self, transmissionKey: _Optional[bytes] = ..., sessionToken: _Optional[bytes] = ..., userId: _Optional[int] = ..., enterpriseUserId: _Optional[int] = ..., deviceName: _Optional[str] = ..., deviceToken: _Optional[bytes] = ..., clientVersionId: _Optional[int] = ..., needUsername: bool = ..., username: _Optional[str] = ..., mspEnterpriseId: _Optional[int] = ..., isPedmAdmin: bool = ..., mcEnterpriseId: _Optional[int] = ...) -> None: ... class RouterDeviceAuth(_message.Message): __slots__ = ("clientId", "clientVersion", "signature", "enterpriseId", "nodeId", "deviceName", "deviceToken", "controllerName", "controllerUid", "ownerUser", "challenge", "ownerId") @@ -328,10 +332,14 @@ class EnforcementType(_message.Message): def __init__(self, enforcementTypeId: _Optional[int] = ..., value: _Optional[str] = ...) -> None: ... class GetEnforcementResponse(_message.Message): - __slots__ = ("enforcementTypes",) + __slots__ = ("enforcementTypes", "addOnIds", "isInTrial") ENFORCEMENTTYPES_FIELD_NUMBER: _ClassVar[int] + ADDONIDS_FIELD_NUMBER: _ClassVar[int] + ISINTRIAL_FIELD_NUMBER: _ClassVar[int] enforcementTypes: _containers.RepeatedCompositeFieldContainer[EnforcementType] - def __init__(self, enforcementTypes: _Optional[_Iterable[_Union[EnforcementType, _Mapping]]] = ...) -> None: ... + addOnIds: _containers.RepeatedScalarFieldContainer[int] + isInTrial: bool + def __init__(self, enforcementTypes: _Optional[_Iterable[_Union[EnforcementType, _Mapping]]] = ..., addOnIds: _Optional[_Iterable[int]] = ..., isInTrial: bool = ...) -> None: ... class PEDMTOTPValidateRequest(_message.Message): __slots__ = ("username", "enterpriseId", "code") diff --git a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py index 884dab15..626e0830 100644 --- a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: ssocloud.proto -# Protobuf Python Version: 5.28.3 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 3, - '', - 'ssocloud.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/version_pb2.py b/keepersdk-package/src/keepersdk/proto/version_pb2.py index 5b002f42..ae1347f8 100644 --- a/keepersdk-package/src/keepersdk/proto/version_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/version_pb2.py @@ -2,21 +2,13 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: version.proto -# Protobuf Python Version: 5.28.2 +# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 28, - 2, - '', - 'version.proto' -) + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() From 0c141774d9b3a0926c5628576d4d0d0effabf2b5 Mon Sep 17 00:00:00 2001 From: sdubey-ks Date: Fri, 22 Aug 2025 12:28:36 +0530 Subject: [PATCH 23/44] Python SDK command examples --- examples/audit_alert/audit_alert_add.py | 115 +++++++++++++++++ examples/audit_alert/audit_alert_delete.py | 110 ++++++++++++++++ examples/audit_alert/audit_alert_edit.py | 117 ++++++++++++++++++ examples/audit_alert/audit_alert_list.py | 109 ++++++++++++++++ examples/audit_alert/audit_alert_view.py | 110 ++++++++++++++++ examples/breachwatch/breachwatch_ignore.py | 110 ++++++++++++++++ examples/breachwatch/breachwatch_scan.py | 110 ++++++++++++++++ .../one_time_share/create_one_time_share.py | 116 +++++++++++++++++ .../one_time_share/list_one_time_shares.py | 112 +++++++++++++++++ .../one_time_share/remove_one_time_share.py | 111 +++++++++++++++++ examples/record/get_command.py | 117 ++++++++++++++++++ 11 files changed, 1237 insertions(+) create mode 100644 examples/audit_alert/audit_alert_add.py create mode 100644 examples/audit_alert/audit_alert_delete.py create mode 100644 examples/audit_alert/audit_alert_edit.py create mode 100644 examples/audit_alert/audit_alert_list.py create mode 100644 examples/audit_alert/audit_alert_view.py create mode 100644 examples/breachwatch/breachwatch_ignore.py create mode 100644 examples/breachwatch/breachwatch_scan.py create mode 100644 examples/one_time_share/create_one_time_share.py create mode 100644 examples/one_time_share/list_one_time_shares.py create mode 100644 examples/one_time_share/remove_one_time_share.py create mode 100644 examples/record/get_command.py diff --git a/examples/audit_alert/audit_alert_add.py b/examples/audit_alert/audit_alert_add.py new file mode 100644 index 00000000..fb74a089 --- /dev/null +++ b/examples/audit_alert/audit_alert_add.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_alert_add(context: KeeperParams, **kwargs): + """ + Execute audit alert add command. + + This function creates a new audit alert + using the Keeper CLI command infrastructure. + """ + audit_alert_add_command = AuditAlertAdd() + + try: + audit_alert_add_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add new audit alert using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_alert_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + alert_name = "alert_name" + frequency = "event" + audit_events = ["login"] + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'name': alert_name, + 'frequency': frequency, + 'audit_event': audit_events, + 'active': 'on' + } + + print(f"Adding new audit alert: {alert_name}") + try: + execute_audit_alert_add(context, **kwargs) + print('Audit alert add completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/audit_alert/audit_alert_delete.py b/examples/audit_alert/audit_alert_delete.py new file mode 100644 index 00000000..770a150a --- /dev/null +++ b/examples/audit_alert/audit_alert_delete.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_alert_delete(context: KeeperParams, **kwargs): + """ + Execute audit alert delete command. + + This function deletes a specific audit alert + using the Keeper CLI command infrastructure. + """ + audit_alert_delete_command = AuditAlertDelete() + + try: + audit_alert_delete_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete audit alert using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_alert_delete.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + alert_target = "alert_id" + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'target': alert_target + } + + print(f"Deleting audit alert: {alert_target}") + try: + execute_audit_alert_delete(context, **kwargs) + print('Audit alert delete completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/audit_alert/audit_alert_edit.py b/examples/audit_alert/audit_alert_edit.py new file mode 100644 index 00000000..b4e78686 --- /dev/null +++ b/examples/audit_alert/audit_alert_edit.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_alert_edit(context: KeeperParams, **kwargs): + """ + Execute audit alert edit command. + + This function edits an existing audit alert + using the Keeper CLI command infrastructure. + """ + audit_alert_edit_command = AuditAlertEdit() + + try: + audit_alert_edit_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Edit audit alert using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_alert_edit.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + alert_target = "alert_id" + new_name = "alert_name" + new_frequency = "1:hour" + audit_events = ["login"] + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'target': alert_target, + 'name': new_name, + 'frequency': new_frequency, + 'audit_event': audit_events, + 'active': 'on' + } + + print(f"Editing audit alert: {alert_target}") + try: + execute_audit_alert_edit(context, **kwargs) + print('Audit alert edit completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/audit_alert/audit_alert_list.py b/examples/audit_alert/audit_alert_list.py new file mode 100644 index 00000000..76e8f3c9 --- /dev/null +++ b/examples/audit_alert/audit_alert_list.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_alert_list(context: KeeperParams, **kwargs): + """ + Execute audit alert list command. + + This function lists all audit alerts in the enterprise + using the Keeper CLI command infrastructure. + """ + audit_alert_list_command = AuditAlertList() + + try: + audit_alert_list_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List audit alerts using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_alert_list.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'format': 'table', # Supported formats: table, json, csv + 'reload': True # If reload is sent, it will be considered True regardless of value, unless set as None + } + + print("Listing audit alerts...") + try: + execute_audit_alert_list(context, **kwargs) + print('Audit alert list completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/audit_alert/audit_alert_view.py b/examples/audit_alert/audit_alert_view.py new file mode 100644 index 00000000..9cf5fa4b --- /dev/null +++ b/examples/audit_alert/audit_alert_view.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_alert_view(context: KeeperParams, **kwargs): + """ + Execute audit alert view command. + + This function views the details of a specific audit alert + using the Keeper CLI command infrastructure. + """ + audit_alert_view_command = AuditAlertView() + + try: + audit_alert_view_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='View audit alert details using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_alert_view.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + alert_target = "alert_id" + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'target': alert_target + } + + print(f"Viewing audit alert details for: {alert_target}") + try: + execute_audit_alert_view(context, **kwargs) + print('Audit alert view completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/breachwatch/breachwatch_ignore.py b/examples/breachwatch/breachwatch_ignore.py new file mode 100644 index 00000000..83b2ec16 --- /dev/null +++ b/examples/breachwatch/breachwatch_ignore.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_breachwatch_ignore(context: KeeperParams, **kwargs): + """ + Execute breachwatch ignore command. + + This function ignores breached passwords for the specified records + using the Keeper CLI command infrastructure. + """ + breachwatch_ignore_command = BreachWatchIgnoreCommand() + + try: + breachwatch_ignore_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Ignore breached passwords for records using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python breachwatch_ignore.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_uids = ["record_uid"] + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'records': record_uids + } + + print(f"Ignoring breached passwords for records: {', '.join(record_uids)}") + try: + execute_breachwatch_ignore(context, **kwargs) + print(f'Breach watch ignore operation completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/breachwatch/breachwatch_scan.py b/examples/breachwatch/breachwatch_scan.py new file mode 100644 index 00000000..907e1e67 --- /dev/null +++ b/examples/breachwatch/breachwatch_scan.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_breachwatch_scan(context: KeeperParams, **kwargs): + """ + Execute breachwatch scan command. + + This function scans for breached passwords in the specified records + using the Keeper CLI command infrastructure. + """ + breachwatch_scan_command = BreachWatchScanCommand() + + try: + breachwatch_scan_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Scan for breached passwords using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python breachwatch_scan.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_uids = ["record_uid"] + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'records': record_uids + } + + print(f"Scanning records for breached passwords: {', '.join(record_uids)}") + try: + execute_breachwatch_scan(context, **kwargs) + print(f'Breach watch scan completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/one_time_share/create_one_time_share.py b/examples/one_time_share/create_one_time_share.py new file mode 100644 index 00000000..a773dac9 --- /dev/null +++ b/examples/one_time_share/create_one_time_share.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_one_time_share_create(context: KeeperParams, **kwargs): + """ + Execute one-time share create command. + + This function creates a one-time share URL for a record + using the Keeper CLI command infrastructure. + """ + one_time_share_create_command = OneTimeShareCreateCommand() + + try: + one_time_share_create_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Create a one-time share URL for a record using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python create_one_time_share.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_name = "record_name" + expire_time = "1h" + share_name = "share_name" + output_destination = "stdout" + is_editable = True + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'record': record_name, + 'expire': expire_time, + 'share_name': share_name, + 'output': output_destination, + 'is_editable': is_editable # If is_editable is sent, it will be considered True regardless of value, unless set as None + } + + print(f"Creating one-time share for record: {record_name}") + print(f"Expiration: {expire_time}, Name: {share_name}, Editable: {is_editable}") + + try: + execute_one_time_share_create(context, **kwargs) + print(f'One-time share URL created successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/one_time_share/list_one_time_shares.py b/examples/one_time_share/list_one_time_shares.py new file mode 100644 index 00000000..485f9a23 --- /dev/null +++ b/examples/one_time_share/list_one_time_shares.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_one_time_share_list(context: KeeperParams, **kwargs): + """ + Execute one-time share list command. + + This function retrieves and displays one-time shares for a record + using the Keeper CLI command infrastructure. + """ + one_time_share_list_command = OneTimeShareListCommand() + + try: + one_time_share_list_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List one-time shares for a record using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python list_one_time_shares.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_name = "record_name" + recursive = None + verbose = None + show_all = None + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'record': record_name, + 'recursive': recursive, # If recursive is sent, it will be considered True regardless of value, unless set as None + 'verbose': verbose, # If verbose is sent, it will be considered True regardless of value, unless set as None + 'show_all': show_all # If show_all is sent, it will be considered True regardless of value, unless set as None + } + + print(f"Listing one-time shares for record: {record_name}") + try: + execute_one_time_share_list(context, **kwargs) + print(f'One-time shares listed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) diff --git a/examples/one_time_share/remove_one_time_share.py b/examples/one_time_share/remove_one_time_share.py new file mode 100644 index 00000000..7eb6d678 --- /dev/null +++ b/examples/one_time_share/remove_one_time_share.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_one_time_share_remove(context: KeeperParams, **kwargs): + """ + Execute one-time share remove command. + + This function removes a one-time share URL for a record + using the Keeper CLI command infrastructure. + """ + one_time_share_remove_command = OneTimeShareRemoveCommand() + + try: + one_time_share_remove_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Remove a one-time share URL for a record using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python remove_one_time_share.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_name = "record_name" + share_id = "share_id" + + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'record': record_name, + 'share': share_id + } + + print(f"Removing one-time share '{share_id}' from record: {record_name}") + + try: + execute_one_time_share_remove(context, **kwargs) + print(f'One-time share removed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + diff --git a/examples/record/get_command.py b/examples/record/get_command.py new file mode 100644 index 00000000..c196dda8 --- /dev/null +++ b/examples/record/get_command.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def get( + context: KeeperParams, + uid: str, +): + """ + Get detailed information about a record/folder/team. + + This function retrieves and displays detailed information about a record/folder/team + using the CLI command infrastructure. It supports different output formats + and can optionally unmask sensitive data. + """ + try: + get_command = RecordGetCommand() + kwargs = { + 'uid': uid, + } + get_command.execute(context=context, **kwargs) + print('Details retrieved successfully!') + return True + + except Exception as e: + print(f'Error getting record/folder/team details: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Get record/folder/team details using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python get_command.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + uid = "record_uid" + + print(f"Note: This example will attempt to get details for record/folder/team '{uid}'") + + try: + context = login_to_keeper_with_config(args.config) + success = get( + context=context, + uid=uid, + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) \ No newline at end of file From e13c65618cceaea1b824d690b89b7735e04ec350 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 22 Aug 2025 17:49:36 +0530 Subject: [PATCH 24/44] Bug Fixes --- .../src/keepercli/commands/audit_alert.py | 14 ++++++++++---- .../src/keepercli/commands/breachwatch.py | 4 ++-- .../src/keepercli/commands/enterprise_user.py | 4 +--- .../src/keepersdk/enterprise/batch_management.py | 4 +--- .../src/keepersdk/vault/ksm_management.py | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/audit_alert.py b/keepercli-package/src/keepercli/commands/audit_alert.py index 75da00df..ff11307b 100644 --- a/keepercli-package/src/keepercli/commands/audit_alert.py +++ b/keepercli-package/src/keepercli/commands/audit_alert.py @@ -149,13 +149,13 @@ def get_alert_configuration(auth: keeper_auth.KeeperAuth, alert_name: Any) -> Di settings = AuditSettingMixin.load_settings(auth) if not settings: raise ValueError(f'Alert with name \"{alert_name}\" not found') - alert_filter = settings.get('AuditAlertFilter') - if not isinstance(alert_filter, list): + alert_filters = settings.get('AuditAlertFilter') + if not isinstance(alert_filters, list): raise ValueError(f'Alert with name \"{alert_name}\" not found') a_number = int(alert_name) if alert_name.isnumeric() else 0 if a_number > 0: - for alert_filter in alert_filter: + for alert_filter in alert_filters: a_id = alert_filter.get('id') if isinstance(a_id, int): if a_id == a_number: @@ -163,7 +163,7 @@ def get_alert_configuration(auth: keeper_auth.KeeperAuth, alert_name: Any) -> Di alerts = [] l_name = alert_name.casefold() - for alert_filter in alert_filter: + for alert_filter in alert_filters: a_name = alert_filter.get('name') or '' if a_name.casefold() == l_name: alerts.append(alert_filter) @@ -339,6 +339,8 @@ def execute(self, context: KeeperParams, **kwargs) -> None: alert = AuditSettingMixin.get_alert_configuration(context.auth, kwargs.get('target')) alert_id: int = alert.get('id') or 0 ctx = AuditSettingMixin.get_alert_context(alert_id) or alert + if not ctx: + raise base.CommandError('No alert found for the given alert ID/name') table = [] header = ['name', 'value'] table.append(['Alert ID', alert_id]) @@ -452,6 +454,8 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: assert context.auth alert = AuditSettingMixin.get_alert_configuration(context.auth, kwargs.get('target')) + if not alert: + raise base.CommandError('No alert found for the given alert ID/name') rq = { 'command': 'delete_enterprise_setting', @@ -562,6 +566,8 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: alert_id = alert.get('id') assert isinstance(alert_id, int) ctx = AuditSettingMixin.get_alert_context(alert_id) or {'id': alert_id} + if not ctx: + raise base.CommandError('No alert found for the given alert ID/name') current_active = 'off' if ctx.get('disabled') is True else 'on' if active != current_active: rq = { diff --git a/keepercli-package/src/keepercli/commands/breachwatch.py b/keepercli-package/src/keepercli/commands/breachwatch.py index 19981d79..9e379ad8 100644 --- a/keepercli-package/src/keepercli/commands/breachwatch.py +++ b/keepercli-package/src/keepercli/commands/breachwatch.py @@ -88,11 +88,11 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: # Parse and resolve record names to UIDs record_names = self._get_record_names(kwargs) if not record_names: - return + raise base.CommandError('Record name or UID is required. Example: breachwatch ignore ') record_uids = self._resolve_record_uids(record_names, context) if not record_uids: - return + raise base.CommandError('Record not found for the given UID/name/path') # Get breached records and their passwords breached_records = self._get_breached_records(vault) diff --git a/keepercli-package/src/keepercli/commands/enterprise_user.py b/keepercli-package/src/keepercli/commands/enterprise_user.py index 24ecf870..d01b4b82 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_user.py +++ b/keepercli-package/src/keepercli/commands/enterprise_user.py @@ -510,14 +510,12 @@ def execute(self, context: KeeperParams, **kwargs) -> None: alias_rq.alias = add_user context.auth.execute_auth_rest('enterprise/enterprise_user_set_primary_alias', alias_rq) else: - add_rq = APIRequest_pb2.EnterpriseUserAddAliasRequestV2() alias_request = APIRequest_pb2.EnterpriseUserAddAliasRequest() alias_request.enterpriseUserId = user.enterprise_user_id alias_request.alias = add_user alias_request.primary = True - add_rq.enterpriseUserAddAliasRequest.append(alias_request) add_rs = context.auth.execute_auth_rest( - 'enterprise/enterprise_user_add_alias', add_rq, response_type=APIRequest_pb2.EnterpriseUserAddAliasResponse) + 'enterprise/enterprise_user_add_alias', alias_request, response_type=APIRequest_pb2.EnterpriseUserAddAliasResponse) assert add_rs for rs in add_rs.status: if rs.status != 'success': diff --git a/keepersdk-package/src/keepersdk/enterprise/batch_management.py b/keepersdk-package/src/keepersdk/enterprise/batch_management.py index fecc6565..2d346a93 100644 --- a/keepersdk-package/src/keepersdk/enterprise/batch_management.py +++ b/keepersdk-package/src/keepersdk/enterprise/batch_management.py @@ -698,11 +698,9 @@ def _to_user_actions(self) -> List[Dict[str, Any]]: rq['enterprise_user_id'] = enterprise_user_id if user_action in {UserAction.Lock, UserAction.Unlock}: rq['command'] = 'enterprise_user_lock' - rq['lock'] = 'locked' if UserAction.Lock else 'unlocked' + rq['lock'] = 'locked' if user_action == UserAction.Lock else 'unlocked' elif user_action == UserAction.ExtendTransfer: rq['command'] = 'extend_account_share_expiration' - elif user_action == UserAction.DisableTfa: - rq['command'] = 'extend_account_share_expiration' else: raise Exception('unsupported action') diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index 43e3b81e..f424b002 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -22,7 +22,7 @@ def list_secrets_manager_apps(vault: vault_online.VaultOnline) -> list[ksm.Secre ) apps_list = [] - if response.applicationSummary: + if response and response.applicationSummary: for app_summary in response.applicationSummary: uid = utils.base64_url_encode(app_summary.appRecordUid) app_record = vault.vault_data.load_record(uid) From efa14454c74b3d80e63694e8f2385381a5e97084 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 29 Aug 2025 17:54:30 +0530 Subject: [PATCH 25/44] Breachwatch password and search record commands --- .../src/keepercli/commands/breachwatch.py | 101 ++++++- .../src/keepercli/commands/record_edit.py | 247 +++++++++++++++++- .../src/keepercli/register_commands.py | 1 + 3 files changed, 345 insertions(+), 4 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/breachwatch.py b/keepercli-package/src/keepercli/commands/breachwatch.py index 9e379ad8..3900b094 100644 --- a/keepercli-package/src/keepercli/commands/breachwatch.py +++ b/keepercli-package/src/keepercli/commands/breachwatch.py @@ -1,5 +1,6 @@ import argparse import base64 +import getpass import json from typing import Any, Optional, Set @@ -28,6 +29,7 @@ def __init__(self): self.register_command(BreachWatchListCommand(), 'list', 'l') self.register_command(BreachWatchIgnoreCommand(), 'ignore') self.register_command(BreachWatchScanCommand(), 'scan') + self.register_command(BreachWatchPasswordCommand(), 'password') class BreachWatchListCommand(base.ArgparseCommand): def __init__(self): @@ -373,4 +375,101 @@ def _perform_breach_watch_scan(self, vault: vault_online.VaultOnline, record_uid logger.error(f"Error scanning record {record_uid}: {str(e)}") def _get_status_display(self, status: int) -> str: - return STATUS_TO_TEXT.get(status, "UNKNOWN") \ No newline at end of file + return STATUS_TO_TEXT.get(status, "UNKNOWN") + + +class BreachWatchPasswordCommand(base.ArgparseCommand): + + PASSWORD_FIELD_WIDTH = 16 + + def __init__(self): + parser = argparse.ArgumentParser(prog='breachwatch password', description='Scan a password against the breach watch database.') + parser.add_argument('passwords', type=str, nargs='*', help='Password') + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs) -> Any: + if not self._is_vault_ready(context): + return + + breach_watch = context.vault.breach_watch_plugin().breach_watch + passwords = self._get_passwords_to_scan(kwargs) + + if not passwords: + raise base.CommandError('No passwords to scan.') + + try: + scan_results = self._scan_passwords(breach_watch, passwords) + self._display_results(scan_results, kwargs.get('passwords')) + self._cleanup_scan_data(breach_watch, scan_results) + except Exception as e: + logger.error(f"Error scanning passwords: {e}") + + def _is_vault_ready(self, context: KeeperParams) -> bool: + """Check if vault and breach watch are properly initialized.""" + if not context.vault: + raise base.CommandError('Vault is not initialized.') + if not context.vault.breach_watch_plugin(): + raise base.CommandError('Breach watch is not enabled. Please contact your administrator to enable this feature.') + return True + + def _get_passwords_to_scan(self, kwargs: dict) -> list[str]: + """Get passwords from command line arguments or prompt user.""" + passwords = kwargs.get('passwords', []) + if passwords: + return passwords + + try: + password = getpass.getpass(prompt='Password to Check: ', stream=None) + if password.strip(): + return [password] + except KeyboardInterrupt: + logger.info('') + return [] + + def _scan_passwords(self, breach_watch, passwords: list[str]) -> list: + """Scan passwords and return results with EUIDs for cleanup.""" + scan_results = [] + for result in breach_watch.scan_passwords(passwords): + if self._is_valid_scan_result(result): + scan_results.append(result) + return scan_results + + def _is_valid_scan_result(self, result) -> bool: + """Validate scan result structure.""" + return result and len(result) == 2 + + def _display_results(self, scan_results: list, echo_passwords: bool) -> None: + """Display scan results in a formatted way.""" + for result in scan_results: + password, scan_result = result + self._display_single_result(password, scan_result, echo_passwords) + + def _display_single_result(self, password: str, scan_result, echo_passwords: bool) -> None: + """Display a single password scan result.""" + pwd = password if echo_passwords else "*" * len(password) + status = self._get_status_text(scan_result) + logger.info(f'{pwd:>{self.PASSWORD_FIELD_WIDTH}s}: {status}') + + def _get_status_text(self, scan_result) -> str: + """Get human-readable status text for scan result.""" + is_breached = getattr(scan_result, 'breachDetected', False) + status_code = client_pb2.BWStatus.BREACHED if is_breached else client_pb2.BWStatus.GOOD + return STATUS_TO_TEXT.get(status_code, "Unknown") + + def _cleanup_scan_data(self, breach_watch, scan_results: list) -> None: + """Clean up scan data by deleting EUIDs.""" + euids = self._extract_euids(scan_results) + if euids: + try: + breach_watch.delete_euids(euids) + except Exception as e: + logger.warning(f"Failed to cleanup scan data: {e}") + + def _extract_euids(self, scan_results: list) -> list: + """Extract EUIDs from scan results for cleanup.""" + euids = [] + for result in scan_results: + password, scan_result = result + if hasattr(scan_result, 'euid') and scan_result.euid: + euids.append(scan_result.euid) + return euids \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index c87bc5f7..6d9882b8 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -6,7 +6,7 @@ import itertools import json import os -from typing import Optional, List, Any, Sequence, Union +from typing import Iterable, Optional, List, Any, Sequence, Union from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519 @@ -17,7 +17,7 @@ from . import base from .. import prompt_utils, api, constants -from ..helpers import folder_utils, record_utils, share_utils, timeout_utils +from ..helpers import folder_utils, record_utils, report_utils, share_utils, timeout_utils from ..params import KeeperParams @@ -1714,4 +1714,245 @@ def _is_sensitive_field_type(self, field_type: str) -> bool: 'password', 'secret', 'otp', 'privateKey', 'pinCode', 'oneTimeCode', 'keyPair', 'licenseNumber' } - return field_type in sensitive_types \ No newline at end of file + return field_type in sensitive_types + + +class RecordSearchCommand(base.ArgparseCommand): + """Command for searching vault records, shared folders, and teams.""" + + DEFAULT_CATEGORIES = 'rst' + MAX_DETAILS_THRESHOLD = 5 + DEFAULT_COLUMN_WIDTH = 40 + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='search', description='Search the vault for records. Can use a regular expression.' + ) + RecordSearchCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + 'pattern', nargs='?', type=str, action='store', help='search pattern' + ) + parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', help='verbose output' + ) + parser.add_argument( + '-c', '--categories', dest='categories', action='store', + help='One or more of these letters for categories to search: "r" = records, ' + '"s" = shared folders, "t" = teams' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Main execution method for the search command.""" + if not context.vault: + raise ValueError('Vault is not initialized. Login to initialize the vault.') + + search_config = self._prepare_search_config(kwargs) + self._perform_search(context.vault, search_config, context) + + def _prepare_search_config(self, kwargs: dict) -> dict: + """Prepare search configuration from command line arguments.""" + pattern = kwargs.get('pattern') or '' + + if pattern == '*': + pattern = '.*' + + verbose = kwargs.get('verbose') is True + + return { + 'pattern': pattern, + 'categories': (kwargs.get('categories') or self.DEFAULT_CATEGORIES).lower(), + 'verbose': verbose, + 'skip_details': not verbose + } + + def _perform_search(self, vault: vault_online.VaultOnline, config: dict, context: KeeperParams): + """Perform the search across all specified categories.""" + # Validate categories + valid_categories = set('rst') + requested_categories = set(config['categories']) + if not requested_categories.issubset(valid_categories): + logger.warning(f"Invalid categories specified: {requested_categories - valid_categories}. " + f"Using valid categories: {requested_categories & valid_categories}") + config['categories'] = ''.join(requested_categories & valid_categories) + + # Store search results for each category + search_results = {} + total_found = 0 + max_results_per_category = 1000 + + # Search in each requested category + if 'r' in config['categories']: + try: + records = context.vault.vault_data.find_records(criteria=config['pattern'], record_type=None, record_version=None) + search_results['records'] = list(itertools.islice(records, max_results_per_category)) + total_found += len(search_results['records']) + except Exception as e: + logger.error(f"Error searching records: {e}") + search_results['records'] = [] + + if 's' in config['categories']: + try: + shared_folders = vault.vault_data.find_shared_folders(criteria=config['pattern']) + search_results['shared_folders'] = list(itertools.islice(shared_folders, max_results_per_category)) + total_found += len(search_results['shared_folders']) + except Exception as e: + logger.error(f"Error searching shared folders: {e}") + search_results['shared_folders'] = [] + + if 't' in config['categories']: + try: + teams = vault.vault_data.find_teams(criteria=config['pattern']) + search_results['teams'] = list(itertools.islice(teams, max_results_per_category)) + total_found += len(search_results['teams']) + except Exception as e: + logger.error(f"Error searching teams: {e}") + search_results['teams'] = [] + + # Check if any objects were found in any of the requested categories + if total_found == 0: + categories_str = ', '.join(requested_categories) + raise base.CommandError(f"No objects found in any of the requested categories: {categories_str}") + + # Display results after all searches are completed + self._display_all_search_results(search_results, config, context, vault) + + def _display_all_search_results(self, search_results: dict, config: dict, context: KeeperParams, vault: vault_online.VaultOnline): + """Display all search results after all searches are completed.""" + if 'records' in search_results and search_results['records']: + logger.info('') + self._display_records_table(search_results['records'], config['verbose']) + + if config['verbose'] and len(search_results['records']) < self.MAX_DETAILS_THRESHOLD: + self._display_record_details(search_results['records'], context) + + if 'shared_folders' in search_results and search_results['shared_folders']: + logger.info('') + self._display_shared_folders(search_results['shared_folders'], config['skip_details'], vault) + + if 'teams' in search_results and search_results['teams']: + logger.info('') + self._display_teams(search_results['teams'], config['skip_details'], vault) + + def _search_records(self, config: dict, context: KeeperParams): + """Search and display records matching the pattern.""" + try: + records = context.vault.vault_data.find_records(criteria=config['pattern'], record_type=None, record_version=None) + + logger.info('') + self._display_records_table(records, config['verbose']) + + if config['verbose'] and len(records) < self.MAX_DETAILS_THRESHOLD: + self._display_record_details(records, context) + except Exception as e: + logger.error(f"Error searching records: {e}") + + def _search_shared_folders(self, vault: vault_online.VaultOnline, config: dict): + """Search and display shared folders matching the pattern.""" + try: + shared_folders = vault.vault_data.find_shared_folders(criteria=config['pattern']) + if shared_folders: + logger.info('') + self._display_shared_folders(shared_folders, config['skip_details'], vault) + except Exception as e: + logger.error(f"Error searching shared folders: {e}") + + def _search_teams(self, vault: vault_online.VaultOnline, config: dict): + """Search and display teams matching the pattern.""" + try: + teams = vault.vault_data.find_teams(criteria=config['pattern']) + if teams: + logger.info('') + self._display_teams(teams, config['skip_details'], vault) + except Exception as e: + logger.error(f"Error searching teams: {e}") + + def _display_records_table(self, records: Iterable[vault_record.KeeperRecordInfo], verbose: bool): + """Display records in a formatted table.""" + table = [] + headers = ['Record UID', 'Type', 'Title', 'Description'] + + for record in records: + row = [ + record.record_uid, + record.record_type, + record.title, + record.description + ] + table.append(row) + + table.sort(key=lambda x: (x[2] or '').lower()) + + column_width = None if verbose else self.DEFAULT_COLUMN_WIDTH + report_utils.dump_report_data( + table, headers, row_number=True, column_width=column_width + ) + + def _display_record_details(self, records: Iterable[vault_record.KeeperRecordInfo], context: KeeperParams): + """Display detailed information for records when verbose mode is enabled.""" + get_command = RecordGetCommand() + for record in records: + kwargs = {'uid': record.record_uid, 'record': True} + get_command.execute(context, **kwargs) + + def _display_shared_folders(self, shared_folders: Iterable[vault_types.SharedFolder], + skip_details: bool, vault: vault_online.VaultOnline): + """Display shared folders in a formatted table with optional details.""" + shared_folders_list = list(shared_folders) + + shared_folders_list.sort(key=lambda x: (x.name or ' ').lower()) + + if shared_folders_list: + self._display_shared_folders_table(shared_folders_list) + + # Display details for small result sets + if len(shared_folders_list) < self.MAX_DETAILS_THRESHOLD and not skip_details: + self._display_shared_folder_details(shared_folders_list, vault) + + def _display_shared_folders_table(self, shared_folders: Iterable[vault_types.SharedFolder]): + """Display shared folders in a formatted table.""" + table = [[i + 1, sf.shared_folder_uid, sf.name] + for i, sf in enumerate(shared_folders)] + report_utils.dump_report_data( + table, headers=["#", 'Shared Folder UID', 'Name'] + ) + logger.info('') + + def _display_shared_folder_details(self, shared_folders: Iterable[vault_types.SharedFolder], + vault: vault_online.VaultOnline): + """Display detailed information for shared folders.""" + get_command = RecordGetCommand() + for sf in shared_folders: + get_command._display_shared_folder_detail(vault=vault, uid=sf.shared_folder_uid) + + def _display_teams(self, teams: Iterable[vault_types.Team], skip_details: bool, + vault: vault_online.VaultOnline): + """Display teams in a formatted table with optional details.""" + teams_list = list(teams) + + teams_list.sort(key=lambda x: (x.name or ' ').lower()) + + if teams_list: + self._display_teams_table(teams_list) + + # Display details for small result sets + if len(teams_list) < self.MAX_DETAILS_THRESHOLD and not skip_details: + self._display_team_details(teams_list, vault) + + def _display_teams_table(self, teams: Iterable[vault_types.Team]): + """Display teams in a formatted table.""" + table = [[i + 1, team.team_uid, team.name] + for i, team in enumerate(teams)] + report_utils.dump_report_data( + table, headers=["#", 'Team UID', 'Name'] + ) + logger.info('') + + def _display_team_details(self, teams: Iterable[vault_types.Team], vault: vault_online.VaultOnline): + """Display detailed information for teams.""" + get_command = RecordGetCommand() + for team in teams: + get_command._display_team_detail(vault=vault, uid=team.team_uid) diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index d9d5d8ca..62d6854d 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -36,6 +36,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('mv', vault_folder.FolderMoveCommand(), base.CommandScope.Vault) commands.register_command('list', vault_record.RecordListCommand(), base.CommandScope.Vault, 'l') commands.register_command('shortcut', vault_record.ShortcutCommand(), base.CommandScope.Vault) + commands.register_command('search', record_edit.RecordSearchCommand(), base.CommandScope.Vault, 's') commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') commands.register_command('record-update', record_edit.RecordUpdateCommand(), base.CommandScope.Vault, 'ru') commands.register_command('rm', record_edit.RecordDeleteCommand(), base.CommandScope.Vault) From 2b44c49cedc20393710e16f42ea369281293c5c9 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 4 Sep 2025 16:39:40 +0530 Subject: [PATCH 26/44] Biometric Commands and Authentication Implemented --- keepercli-package/requirements.txt | 5 + keepercli-package/setup.cfg | 5 + .../src/keepercli/biometric/README.md | 390 ++++++++++++++++++ .../src/keepercli/biometric/__init__.py | 51 +++ .../src/keepercli/biometric/client.py | 277 +++++++++++++ .../keepercli/biometric/commands/__init__.py | 0 .../src/keepercli/biometric/commands/base.py | 186 +++++++++ .../src/keepercli/biometric/commands/list.py | 57 +++ .../keepercli/biometric/commands/register.py | 125 ++++++ .../biometric/commands/unregister.py | 226 ++++++++++ .../biometric/commands/update_name.py | 150 +++++++ .../keepercli/biometric/commands/verify.py | 241 +++++++++++ .../keepercli/biometric/platforms/__init__.py | 0 .../src/keepercli/biometric/platforms/base.py | 151 +++++++ .../keepercli/biometric/platforms/detector.py | 63 +++ .../biometric/platforms/macos/__init__.py | 3 + .../biometric/platforms/macos/handler.py | 260 ++++++++++++ .../biometric/platforms/macos/keychain.py | 209 ++++++++++ .../biometric/platforms/macos/webauthn.py | 368 +++++++++++++++++ .../keepercli/biometric/platforms/windows.py | 246 +++++++++++ .../src/keepercli/biometric/utils/__init__.py | 0 .../src/keepercli/biometric/utils/aaguid.py | 41 ++ .../keepercli/biometric/utils/constants.py | 121 ++++++ .../biometric/utils/error_handler.py | 130 ++++++ .../src/keepercli/commands/breachwatch.py | 2 +- keepercli-package/src/keepercli/login.py | 70 +++- .../src/keepercli/register_commands.py | 2 + .../keepersdk/authentication/login_auth.py | 5 +- 28 files changed, 3381 insertions(+), 3 deletions(-) create mode 100644 keepercli-package/src/keepercli/biometric/README.md create mode 100644 keepercli-package/src/keepercli/biometric/__init__.py create mode 100644 keepercli-package/src/keepercli/biometric/client.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/__init__.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/base.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/list.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/register.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/unregister.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/update_name.py create mode 100644 keepercli-package/src/keepercli/biometric/commands/verify.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/__init__.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/base.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/detector.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/macos/__init__.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/macos/handler.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/macos/keychain.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/macos/webauthn.py create mode 100644 keepercli-package/src/keepercli/biometric/platforms/windows.py create mode 100644 keepercli-package/src/keepercli/biometric/utils/__init__.py create mode 100644 keepercli-package/src/keepercli/biometric/utils/aaguid.py create mode 100644 keepercli-package/src/keepercli/biometric/utils/constants.py create mode 100644 keepercli-package/src/keepercli/biometric/utils/error_handler.py diff --git a/keepercli-package/requirements.txt b/keepercli-package/requirements.txt index dcb9bfab..96bd2998 100644 --- a/keepercli-package/requirements.txt +++ b/keepercli-package/requirements.txt @@ -4,3 +4,8 @@ pyperclip tabulate asciitree colorama +cbor2; sys_platform == "darwin" and python_version>='3.10' +pyobjc-framework-LocalAuthentication; sys_platform == "darwin" and python_version>='3.10' +winrt-runtime; sys_platform == "win32" +winrt-Windows.Foundation; sys_platform == "win32" +winrt-Windows.Security.Credentials.UI; sys_platform == "win32" diff --git a/keepercli-package/setup.cfg b/keepercli-package/setup.cfg index 507f99d1..3d8c41bf 100644 --- a/keepercli-package/setup.cfg +++ b/keepercli-package/setup.cfg @@ -31,6 +31,11 @@ install_requires = tabulate asciitree colorama + cbor2; sys_platform == "darwin" and python_version>='3.10' + pyobjc-framework-LocalAuthentication; sys_platform == "darwin" and python_version>='3.10' + winrt-runtime; sys_platform == "win32" + winrt-Windows.Foundation; sys_platform == "win32" + winrt-Windows.Security.Credentials.UI; sys_platform == "win32" [options.package_data] keepercli = diff --git a/keepercli-package/src/keepercli/biometric/README.md b/keepercli-package/src/keepercli/biometric/README.md new file mode 100644 index 00000000..c0d80b9c --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/README.md @@ -0,0 +1,390 @@ +# Biometric Authentication + +## Secure Authentication with Platform Biometrics for Keeper Commander + +The Biometric Authentication module for Keeper Commander enables users to authenticate using platform-specific biometric methods such as Windows Hello and Touch ID. This module provides a secure, convenient alternative to password-based authentication while maintaining the highest security standards through FIDO2/WebAuthn protocols. + +### Core Functionality + +- **Cross-Platform Support**: Windows Hello and Touch ID integration +- **WebAuthn Protocol**: FIDO2-compliant authentication implementation +- **Secure Credential Storage**: Platform-native secure storage (Windows Hello, macOS Keychain) +- **Device Trust Management**: Required device registration for security +- **Credential Lifecycle**: Complete registration, verification, and removal workflows +- **Error Handling**: Comprehensive error handling with user-friendly messages + +--- + +## Prerequisites + +### OS-Level Biometric Setup + +**Before using biometric authentication with Keeper Commander, you must have biometric credentials already configured in your operating system:** + +#### Windows Requirements: +- **Windows 11** (required for biometric authentication support) +- **Windows Hello must be set up** in Windows Settings +- Navigate to: `Settings > Accounts > Sign-in options > Windows Hello` +- Configure at least one of: + - **Face recognition**: Set up Windows Hello Face + - **Fingerprint**: Set up Windows Hello Fingerprint + - **PIN**: Required as a backup authentication method +- Hardware requirements: + - Compatible biometric hardware (fingerprint reader, IR camera, etc.) + +#### macOS Requirements: +- **Touch ID must be enabled** in System Preferences +- Navigate to: `System Preferences > Touch ID & Password` +- Add your fingerprint(s) to the system +- Hardware requirements: + - Mac with Touch ID sensor (MacBook Pro 2016+, MacBook Air 2018+, iMac with Touch ID, etc.) + +### Software Dependencies + +Install the required Python packages: + +```bash +pip install cbor2 pyobjc-framework-LocalAuthentication fido2 +``` + +--- + +## Usage + +### Initial Setup Process + +#### 1. Biometric Registration + +Register your biometric credentials with Keeper (requires initial login with your Master password): + +```bash +# First, log in to Keeper Commander +keeper shell + +# Register biometric authentication +biometric register +``` + +You'll be prompted to: +- Complete biometric authentication (Touch ID/Windows Hello) +- Provide a friendly name for the credential (optional) + +Example with custom settings: + +```bash +biometric register --name "My MacBook" +``` + +#### 2. Device Registration (Mandatory) + +**Device registration is required before biometric authentication can be used:** + +```bash +# Register your device with Keeper (mandatory step) +this-device register +``` + +**Why Device Registration is Required:** +- Biometric authentication requires a trusted device relationship +- The device must be approved by Keeper's security system +- Without device registration, biometric login will fall back to default authentication + +--- + +## Commands + +### Available Commands + +| Command | Description | +|---------|-------------| +| `biometric register` | Add biometric authentication method | +| `biometric list` | List registered biometric authentication methods | +| `biometric verify` | Test biometric authentication without logging in | +| `biometric unregister` | Remove biometric authentication from account | +| `biometric update-name` | Update friendly name of a biometric passkey | + +### Register Command + +Add a new biometric authentication method: + +```bash +biometric register [options] +``` + +**Parameters:** +- `--name`: Friendly name for the biometric method + +**Examples:** + +```bash +# Basic registration with default settings +biometric register + +# Registration with custom name +biometric register --name "Work Laptop" +``` + +**Sample Output:** + +``` +Adding biometric authentication method: My MacBook +Please complete biometric authentication... +Biometric authentication completed successfully! + +Success! Biometric authentication "Commander CLI (MacBook)" has been registered. + +Please register your device using the "this-device register" command to set biometric authentication as your default login method. +``` + +### List Command + +Display all registered biometric authentication methods: + +```bash +biometric list +``` + +**Sample Output:** + +``` +Registered Biometric Authentication Methods: +---------------------------------------------------------------------- +Name: Commander CLI (MacBook) +Created: December 20, 2023 +Last Used: Today +---------------------------------------------------------------------- +Name: iCloud Keychain +Created: December 18, 2023 +Last Used: July 10, 2025 +---------------------------------------------------------------------- +Name: Chrome on Mac +Created: November 15, 2023 +Last Used: Never +---------------------------------------------------------------------- +``` + +### Verify Command + +Test biometric authentication without performing a login: + +```bash +biometric verify +``` + +**Sample Output:** + +``` +Please complete biometric authentication... + +Biometric Authentication Verification Results: +================================================== +Status: SUCCESSFUL +Purpose: LOGIN +Login Token: Received + +Your biometric authentication is working correctly! +================================================== +``` + +### Unregister Command + +Remove biometric authentication from your account: + +```bash +biometric unregister [options] +``` + +**Parameters:** +- `--confirm`: Skip confirmation prompt + +**Examples:** + +```bash +# Interactive unregistration (with confirmation) +biometric unregister + +# Silent unregistration (no confirmation) +biometric unregister --confirm +``` + +**Sample Output:** + +``` +Are you sure you want to disable biometric authentication for user 'user@example.com'? (y/n): y + +Biometric authentication has been completely removed for user 'user@example.com'. +Default authentication will be used for future logins. +``` + +### Update Name Command + +Update the friendly name of an existing biometric credential: + +```bash +biometric update-name +``` + +This command provides an interactive interface to: +1. Select from available credentials +2. Enter a new friendly name (max 32 characters) +3. Confirm the update + +**Sample Interaction:** + +``` +Found 2 biometric credential(s) with friendly names + +Available Biometric Credentials: +-------------------------------------------------- + 1. Commander CLI (MacBook) + Created: January 15, 2024 + Last Used: Today + + 2. Commander CLI (Desktop) + Created: January 10, 2024 + Last Used: January 18, 2024 + +Select credential number (1-2): 1 +Selected: Commander CLI (MacBook) + +Current Name: Commander CLI (MacBook) +Enter a new friendly name (max 32 characters): +New name: Personal MacBook + +Update Summary: +-------------------- +Current Name: Commander CLI (MacBook) +New Name: Personal MacBook + +Proceed with update? (y/n): y + +Passkey Update Results: +============================== +Status: Success +Old Name: Commander CLI (MacBook) +New Name: Personal MacBook +Message: Passkey friendly name was successfully updated +============================== +``` + +--- + +## Platform Support + +### Windows + +**Supported Methods:** +- Windows Hello Face recognition +- Windows Hello Fingerprint +- Windows Hello PIN (as backup) +- WebAuthn support via Windows Hello + +**Setup Requirements:** +- Windows 11 (required for biometric authentication support) +- Windows Hello configured in Settings > Accounts > Sign-in options +- At least one enrolled biometric factor (Face/Fingerprint) OR PIN +- Compatible biometric hardware (for Face/Fingerprint) +- Administrative privileges for initial Windows Hello setup + +### macOS + +**Supported Methods:** +- Touch ID +- Custom macOS WebAuthn client integration +- Keychain-based credential management + +**Setup Requirements:** +- Touch ID enabled in System Preferences > Touch ID & Password +- At least one enrolled fingerprint in system settings +- Compatible Mac with Touch ID sensor (MacBook Pro 2016+, MacBook Air 2018+, iMac with Touch ID, etc.) +- LocalAuthentication framework dependencies + +--- + +## Troubleshooting + +### Common Issues + +#### "Authentication failed" Error + +**Possible Causes:** +- Biometric sensors are dirty or obstructed +- OS-level biometric credentials need re-enrollment +- Hardware compatibility issues + +**Solutions:** +```bash +# Re-register biometric authentication +biometric unregister --confirm +biometric register + +# Verify OS biometric setup is working +# Windows: Test Windows Hello in Settings +# macOS: Test Touch ID in System Preferences +``` + +#### "No biometric hardware detected" Error + +**Windows Solutions:** +```bash +# Check Windows Hello setup status +# Navigate to Settings > Accounts > Sign-in options > Windows Hello + +# Verify biometric enrollment +# Check if Face recognition or Fingerprint is set up +# Ensure at least PIN is configured as backup + +# Install FIDO2 libraries if missing +pip install fido2 +``` + +**macOS Solutions:** +```bash +# Check Touch ID setup comprehensively +# Navigate to System Preferences > Touch ID & Password + +# Install required dependencies +pip install cbor2 pyobjc-framework-LocalAuthentication + +# Test Touch ID detection manually +bioutil -r -s +``` + +#### "Password prompt after biometric" Issue + +**Cause:** Device not registered with Keeper + +**Solution:** +```bash +# Register device first (mandatory step) +this-device register + +# Then try biometric authentication +biometric verify +``` + +#### "Credential already exists" Error + +**Solution:** +```bash +# Remove existing credential first +biometric unregister --confirm + +# Register new credential +biometric register +``` + +--- + + +## Support + +For support or feature requests regarding biometric authentication, please contact: + +• **Email**: commander@keepersecurity.com + +If you encounter issues with biometric authentication or would like to request additional platform support, please reach out with: +- Operating system and version +- Hardware specifications +- Error messages or logs +- Steps to reproduce the issue diff --git a/keepercli-package/src/keepercli/biometric/__init__.py b/keepercli-package/src/keepercli/biometric/__init__.py new file mode 100644 index 00000000..1b1cd5c1 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/__init__.py @@ -0,0 +1,51 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Dict[str, Any]: + """Generate registration options from Keeper API""" + try: + rq = APIRequest_pb2.PasskeyRegistrationRequest() + rq.authenticatorAttachment = APIRequest_pb2.AuthenticatorAttachment.PLATFORM + + rs = vault.keeper_auth.execute_auth_rest( + rest_endpoint=API_ENDPOINTS['generate_registration'], request=rq, response_type=APIRequest_pb2.PasskeyRegistrationResponse + ) + + return { + 'challenge_token': rs.challengeToken, + 'creation_options': json.loads(rs.pkCreationOptions) + } + except Exception as e: + raise Exception(str(e)) + + def create_credential(self, registration_options: Dict[str, Any]): + """Create biometric credential""" + if not self.platform_handler: + raise Exception("Platform handler not available") + + try: + creation_options = registration_options['creation_options'] + + if isinstance(creation_options.get('challenge'), str): + creation_options['challenge'] = utils.base64_url_decode(creation_options['challenge']) + + # Handle platform-specific options + creation_options = self.platform_handler.handle_credential_creation(creation_options) + + # Create WebAuthn client + options = PublicKeyCredentialCreationOptions.from_dict(creation_options) + rp_id = options.rp.id or creation_options.get('rp', {}).get('id') + if not rp_id: + raise Exception("No RP ID found in API response - server configuration error") + origin = f'{WEBAUTHN_ORIGIN_SCHEME}://{rp_id}' + + data_collector = DefaultClientDataCollector(origin) + client = self.platform_handler.create_webauthn_client(data_collector) + + print("Please complete biometric authentication...") + return self.platform_handler.perform_credential_creation(client, options) + + except Exception as e: + raise Exception(str(e)) + + def verify_registration(self, context: KeeperParams, registration_options: Dict[str, Any], + credential_response, friendly_name: str): + """Verify registration with Keeper API""" + try: + client_data_bytes = credential_response.response.client_data + if hasattr(client_data_bytes, 'b64'): + client_data_b64 = client_data_bytes.b64 + else: + client_data_b64 = utils.base64_url_encode(client_data_bytes) + + attestation_object = { + 'id': credential_response.id, + 'rawId': utils.base64_url_encode(credential_response.raw_id), + 'response': { + 'attestationObject': utils.base64_url_encode(credential_response.response.attestation_object), + 'clientDataJSON': client_data_b64 + }, + 'type': 'public-key', + 'clientExtensionResults': credential_response.client_extension_results or {} + } + + rq = APIRequest_pb2.PasskeyRegistrationFinalization() + rq.challengeToken = registration_options['challenge_token'] + rq.authenticatorResponse = json.dumps(attestation_object) + rq.friendlyName = friendly_name + + context.vault.keeper_auth.execute_auth_rest(rest_endpoint=API_ENDPOINTS['verify_registration'], request=rq) + + if self.platform_handler and hasattr(self.platform_handler, 'storage_handler'): + storage_handler = getattr(self.platform_handler, 'storage_handler') + if storage_handler and hasattr(storage_handler, 'store_credential_id'): + try: + credential_id = credential_response.id + success = storage_handler.store_credential_id(context.username, credential_id) + if success: + logging.debug("Stored credential ID for user: %s", context.username) + else: + logging.warning("Failed to store credential ID for user: %s", context.username) + except Exception as e: + logging.warning("Error storing credential ID: %s", str(e)) + + except Exception as e: + raise Exception(str(e)) + + def generate_authentication_options(self, context: KeeperParams, purpose: str = 'login') -> Dict[str, Any]: + """Generate authentication options""" + try: + rq = APIRequest_pb2.PasskeyAuthenticationRequest() + rq.authenticatorAttachment = APIRequest_pb2.AuthenticatorAttachment.PLATFORM + rq.clientVersion = context.auth.keeper_endpoint.client_version + rq.username = context.username + rq.passkeyPurpose = (APIRequest_pb2.PasskeyPurpose.PK_REAUTH + if purpose == 'vault' else APIRequest_pb2.PasskeyPurpose.PK_LOGIN) + + if context.auth.auth_context.device_token: + rq.encryptedDeviceToken = context.auth.auth_context.device_token + + rs = context.vault.keeper_auth.execute_auth_rest(rest_endpoint=API_ENDPOINTS['generate_authentication'], request=rq, response_type=APIRequest_pb2.PasskeyAuthenticationResponse) + + return { + 'challenge_token': rs.challengeToken, + 'request_options': json.loads(rs.pkRequestOptions), + 'login_token': rs.encryptedLoginToken, + 'purpose': purpose + } + except Exception as e: + raise Exception(str(e)) + + def generate_login_authentication_options(self, login_auth: login_auth.LoginAuth, client_version: str, username: str, purpose: str = 'login', device_token: Optional[str] = None) -> Dict[str, Any]: + """Generate authentication options""" + try: + rq = APIRequest_pb2.PasskeyAuthenticationRequest() + rq.authenticatorAttachment = APIRequest_pb2.AuthenticatorAttachment.PLATFORM + rq.clientVersion = client_version + rq.username = username + rq.passkeyPurpose = (APIRequest_pb2.PasskeyPurpose.PK_REAUTH + if purpose == 'vault' else APIRequest_pb2.PasskeyPurpose.PK_LOGIN) + + if login_auth.context.device_token: + rq.encryptedDeviceToken = login_auth.context.device_token + + rs = login_auth.execute_rest(rest_endpoint=API_ENDPOINTS['generate_authentication'], request=rq, response_type=APIRequest_pb2.PasskeyAuthenticationResponse) + + return { + 'challenge_token': rs.challengeToken, + 'request_options': json.loads(rs.pkRequestOptions), + 'login_token': rs.encryptedLoginToken, + 'purpose': purpose + } + except Exception as e: + raise Exception(str(e)) + + def perform_authentication(self, auth_options: Dict[str, Any]): + """Perform biometric authentication""" + if not self.platform_handler: + raise Exception("Platform handler not available") + + try: + request_options = auth_options['request_options'] + pk_options = request_options.get('publicKeyCredentialRequestOptions', request_options) + + if not isinstance(pk_options['challenge'], (bytes, bytearray)): + pk_options['challenge'] = utils.base64_url_decode(pk_options['challenge']) + + if 'allowCredentials' in pk_options: + for cred in pk_options['allowCredentials']: + if isinstance(cred.get('id'), str): + cred['id'] = utils.base64_url_decode(cred['id']) + + pk_options = self.platform_handler.handle_authentication_options(pk_options) + + options = PublicKeyCredentialRequestOptions.from_dict(pk_options) + rp_id = options.rp_id or pk_options.get('rpId') + if not rp_id: + raise Exception("No RP ID found in API response - server configuration error") + origin = f'{WEBAUTHN_ORIGIN_SCHEME}://{rp_id}' + + data_collector = DefaultClientDataCollector(origin) + client = self.platform_handler.create_webauthn_client(data_collector) + + return self.platform_handler.perform_authentication(client, options) + + except Exception as e: + raise Exception(str(e)) + + def get_available_credentials(self, vault: vault_online.VaultOnline): + """Get list of available biometric credentials""" + try: + rq = APIRequest_pb2.PasskeyListRequest() + + rs = vault.keeper_auth.execute_auth_rest(rest_endpoint=API_ENDPOINTS['get_available_keys'], request=rq, response_type=APIRequest_pb2.PasskeyListResponse) + + return [{ + 'id': passkey.userId, + 'name': passkey.friendlyName, + 'created': passkey.createdAtMillis, + 'last_used': passkey.lastUsedMillis, + 'credential_id': passkey.credentialId, + 'aaguid': getattr(passkey, 'AAGUID', None) + } for passkey in rs.passkeyInfo] + + except Exception as e: + raise Exception(str(e)) + + def disable_passkey(self, vault: vault_online.VaultOnline, user_id: int, credential_id: bytes): + """Disable a passkey using the UpdatePasskeyRequest endpoint""" + try: + rq = APIRequest_pb2.UpdatePasskeyRequest() + rq.userId = user_id + rq.credentialId = credential_id + + vault.keeper_auth.execute_auth_rest(rest_endpoint=API_ENDPOINTS['disable_passkey'], request=rq) + + return {'status': STATUS_SUCCESS, 'message': API_RESPONSE_MESSAGES['passkey_disabled_success']} + + except Exception as e: + error_msg = str(e).lower() + if 'bad_request' in error_msg or 'credential id' in error_msg or 'userid' in error_msg: + return {'status': STATUS_BAD_REQUEST, 'message': API_RESPONSE_MESSAGES['disable_bad_request']} + elif 'server_error' in error_msg or 'unexpected' in error_msg: + return {'status': STATUS_ERROR, 'message': API_RESPONSE_MESSAGES['server_exception']} + else: + raise Exception(str(e)) + + def update_passkey_name(self, vault: vault_online.VaultOnline, user_id: int, credential_id: bytes, friendly_name: str): + """Update the friendly name of a passkey using the UpdatePasskeyRequest endpoint""" + try: + rq = APIRequest_pb2.UpdatePasskeyRequest() + rq.userId = user_id + rq.credentialId = credential_id + rq.friendlyName = friendly_name + + vault.keeper_auth.execute_auth_rest(rest_endpoint=API_ENDPOINTS['update_passkey_name'], request=rq) + + return {'status': STATUS_SUCCESS, 'message': API_RESPONSE_MESSAGES['passkey_name_updated_success']} + + except Exception as e: + error_msg = str(e).lower() + if 'bad_request' in error_msg or 'credential id' in error_msg or 'userid' in error_msg: + return {'status': STATUS_BAD_REQUEST, 'message': API_RESPONSE_MESSAGES['update_bad_request']} + elif 'server_error' in error_msg or 'unexpected' in error_msg: + return {'status': STATUS_ERROR, 'message': API_RESPONSE_MESSAGES['server_exception']} + else: + raise Exception(str(e)) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/commands/__init__.py b/keepercli-package/src/keepercli/biometric/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keepercli-package/src/keepercli/biometric/commands/base.py b/keepercli-package/src/keepercli/biometric/commands/base.py new file mode 100644 index 00000000..049ee6ae --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/commands/base.py @@ -0,0 +1,186 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' str: + """Generate default credential name""" + hostname = platform.node() or 'Unknown' + + prefix = "Commander CLI (" + suffix = ")" + max_hostname_length = 31 - len(prefix) - len(suffix) + + if len(hostname) > max_hostname_length: + hostname = hostname[:max_hostname_length] + + return CREDENTIAL_NAME_TEMPLATE.format(hostname=hostname) + + def _format_timestamp(self, timestamp_ms: int) -> str: + """Format timestamp for display with friendly date format""" + if not timestamp_ms: + return 'Never' + + timestamp_s = timestamp_ms / 1000 + dt = datetime.fromtimestamp(timestamp_s) + today = date.today() + + if dt.date() == today: + return 'Today' + + # Format as "Month Day, Year" (e.g., "July 10, 2025") + return dt.strftime('%B %d, %Y') + + def _check_platform_support(self, force: bool = False): + """Check if platform supports biometric authentication""" + if not FIDO2_AVAILABLE: + raise CommandError(ERROR_MESSAGES['no_fido2']) + + supported, message = self.detector.detect_platform_capabilities() + + if not supported and not force: + raise CommandError(f'{ERROR_MESSAGES["platform_not_supported"]}: {message}') + + return supported, message + + def _check_biometric_flag(self, username: str) -> bool: + """Check if biometric authentication is enabled for user""" + try: + handler = self.detector.get_platform_handler() + return handler.get_biometric_flag(username) + except Exception: + return False + + def _delete_biometric_flag(self, username: str) -> bool: + """Delete biometric authentication flag for user""" + try: + handler = self.detector.get_platform_handler() + return handler.delete_biometric_flag(username) + except Exception: + return False + + def _get_available_credentials_or_error(self, vault: vault_online.VaultOnline): + """Get available credentials with consistent error handling""" + try: + credentials = self.client.get_available_credentials(vault) + if not credentials: + raise CommandError(ERROR_MESSAGES['no_credentials']) + return credentials + except Exception as e: + raise CommandError(str(e)) + + def _execute_with_error_handling(self, operation: str, func, *args, **kwargs): + """Execute a function with consistent error handling""" + return BiometricErrorHandler.execute_with_error_handling( + self.__class__.__name__, operation, func, *args, **kwargs + ) + + +class BiometricCommand(GroupCommand): + """Base class for biometric commands with common functionality""" + + def __init__(self): + super().__init__('biometric') + self.client = BiometricClient() + self.detector = BiometricDetector() + + def _get_default_credential_name(self) -> str: + """Generate default credential name""" + hostname = platform.node() or 'Unknown' + + prefix = "Commander CLI (" + suffix = ")" + max_hostname_length = 31 - len(prefix) - len(suffix) + + if len(hostname) > max_hostname_length: + hostname = hostname[:max_hostname_length] + + return CREDENTIAL_NAME_TEMPLATE.format(hostname=hostname) + + def _format_timestamp(self, timestamp_ms: int) -> str: + """Format timestamp for display with friendly date format""" + if not timestamp_ms: + return 'Never' + + timestamp_s = timestamp_ms / 1000 + dt = datetime.fromtimestamp(timestamp_s) + today = date.today() + + if dt.date() == today: + return 'Today' + + # Format as "Month Day, Year" (e.g., "July 10, 2025") + return dt.strftime('%B %d, %Y') + + def _check_platform_support(self, force: bool = False): + """Check if platform supports biometric authentication""" + if not FIDO2_AVAILABLE: + raise CommandError(ERROR_MESSAGES['no_fido2']) + + supported, message = self.detector.detect_platform_capabilities() + + if not supported and not force: + raise CommandError(f'{ERROR_MESSAGES["platform_not_supported"]}: {message}') + + return supported, message + + def _check_biometric_flag(self, username: str) -> bool: + """Check if biometric authentication is enabled for user""" + try: + handler = self.detector.get_platform_handler() + return handler.get_biometric_flag(username) + except Exception: + return False + + def _delete_biometric_flag(self, username: str) -> bool: + """Delete biometric authentication flag for user""" + try: + handler = self.detector.get_platform_handler() + return handler.delete_biometric_flag(username) + except Exception: + return False + + def _get_available_credentials_or_error(self, vault: vault_online.VaultOnline): + """Get available credentials with consistent error handling""" + try: + credentials = self.client.get_available_credentials(vault) + if not credentials: + raise CommandError(ERROR_MESSAGES['no_credentials']) + return credentials + except Exception as e: + raise CommandError(str(e)) + + def _execute_with_error_handling(self, operation: str, func, *args, **kwargs): + """Execute a function with consistent error handling""" + return BiometricErrorHandler.execute_with_error_handling( + self.__class__.__name__, operation, func, *args, **kwargs + ) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/commands/list.py b/keepercli-package/src/keepercli/biometric/commands/list.py new file mode 100644 index 00000000..aee13ce0 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/commands/list.py @@ -0,0 +1,57 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 32: + raise ValueError("Friendly name must be 32 characters or less") + + logging.info("Adding biometric authentication method: %s", friendly_name) + + return { + 'friendly_name': friendly_name + } + + def _perform_registration(self, context: KeeperParams, registration_data): + """Perform the actual biometric registration""" + try: + # Generate registration options + registration_options = self.client.generate_registration_options(context.vault) + + # Create credential + credential_response = self.client.create_credential(registration_options) + + # Verify registration + self.client.verify_registration(context, registration_options, credential_response, registration_data['friendly_name']) + + return { + 'response': credential_response, + 'friendly_name': registration_data['friendly_name'] + } + + except Exception as e: + return self._handle_registration_error(e, context.username, registration_data['friendly_name']) + + def _handle_registration_error(self, error, username, friendly_name): + """Handle registration errors, including existing credential scenarios""" + error_str = str(error).lower() + if ("object already exists" in error_str or + "biometric credential for this account already exists" in error_str): + + self._store_placeholder_credential(username) + return {'friendly_name': friendly_name, 'existing_credential': True} + else: + raise error + + def _store_placeholder_credential(self, username: str): + """Store placeholder credential ID if storage is available""" + if self.client.platform_handler and hasattr(self.client.platform_handler, 'storage_handler'): + storage_handler = getattr(self.client.platform_handler, 'storage_handler') + if storage_handler and hasattr(storage_handler, 'store_credential_id'): + existing_credential_id = storage_handler.get_credential_id(username) + if not existing_credential_id: + placeholder_id = f"{username}" + storage_handler.store_credential_id(username, placeholder_id) + logging.debug("Stored placeholder credential ID for user: %s", username) + else: + logging.debug("Credential ID already exists for user: %s", username) + + def _finalize_registration(self, username: str, credential): + """Finalize registration and report success""" + friendly_name = credential['friendly_name'] + self._report_success(friendly_name, username) + + def _report_success(self, friendly_name: str, username: str): + """Report successful registration""" + if self._check_biometric_flag(username): + logging.info(SUCCESS_MESSAGES['registration_complete']) + print(f'\nSuccess! Biometric authentication "{friendly_name}" has been registered.') + print(f'\nPlease register your device using the \033[31m"this-device register"\033[0m command to set biometric authentication as your default login method.') + else: + print(f'\nBiometric authentication setup incomplete. Please try again.') \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/commands/unregister.py b/keepercli-package/src/keepercli/biometric/commands/unregister.py new file mode 100644 index 00000000..c5c6f58c --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/commands/unregister.py @@ -0,0 +1,226 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' bool: + """Get user confirmation for unregistering biometric authentication""" + confirm = input(f"Are you sure you want to disable biometric authentication for user '{username}'? (y/n): ") + return confirm.lower() == 'y' + + def _get_rp_id_from_server(self, context: KeeperParams) -> Optional[str]: + """Get RP ID from server authentication options""" + try: + auth_options = self.client.generate_authentication_options(context, 'login') + + request_options = auth_options['request_options'] + pk_options = request_options.get('publicKeyCredentialRequestOptions', request_options) + rp_id = pk_options.get('rpId') + + if not rp_id: + print("Warning: Could not get RP ID from server - using limited cleanup") + return None + + return rp_id + except Exception as e: + print(f"Warning: Could not get RP ID from server ({str(e)}) - using limited cleanup") + return None + + def _disable_server_passkeys(self, context: KeeperParams): + """Disable the specific passkey stored for this device""" + try: + stored_credential_id = self._get_stored_credential_id(context.username) + + if stored_credential_id: + passkey_result = self._disable_specific_passkey(context.vault, stored_credential_id) + self._process_specific_passkey_result(passkey_result) + else: + print("Warning: No stored credential ID found for this device.") + print("This could mean:") + print(" - The credential was already removed") + print(" - Registration was incomplete") + print("") + print("No passkeys will be disabled on the server.") + + except Exception as e: + print(f"Failed to disable passkey on server: {str(e)}") + + def _get_stored_credential_id(self, username: str) -> Optional[str]: + """Get the stored credential ID for this device""" + try: + platform_handler = self.client.platform_handler + if platform_handler and hasattr(platform_handler, 'storage_handler'): + storage_handler = getattr(platform_handler, 'storage_handler') + if storage_handler and hasattr(storage_handler, 'get_credential_id'): + return storage_handler.get_credential_id(username) + except Exception as e: + print(f"Warning: Could not retrieve stored credential ID: {str(e)}") + return None + + def _disable_specific_passkey(self, vault: vault_online.VaultOnline, credential_id: str): + """Disable a specific passkey by credential ID""" + try: + available_credentials = self.client.get_available_credentials(vault) + + target_passkey = None + for credential in available_credentials: + stored_cred_id_bytes = credential.get('credential_id') + if isinstance(stored_cred_id_bytes, bytes): + from keepersdk import utils + stored_cred_id_b64 = utils.base64_url_encode(stored_cred_id_bytes) + if stored_cred_id_b64 == credential_id or credential_id == stored_cred_id_bytes: + target_passkey = credential + break + elif credential_id == stored_cred_id_bytes: + target_passkey = credential + break + + if target_passkey: + result = self.client.disable_passkey(vault, target_passkey['id'], target_passkey['credential_id']) + return result + else: + return {'status': STATUS_NOT_FOUND, 'message': f'Passkey with credential ID {credential_id} not found on server'} + + except Exception as e: + return {'status': STATUS_ERROR, 'message': f'Error disabling specific passkey: {str(e)}'} + + def _process_specific_passkey_result(self, passkey_result): + """Process and display results for a specific passkey disable operation""" + if isinstance(passkey_result, dict): + status = passkey_result.get('status') + message = passkey_result.get('message', 'Unknown result') + + if status == STATUS_NOT_FOUND: + pass + elif status == STATUS_SUCCESS: + pass + else: + print(f"Failed to disable passkey: {message}") + else: + print(f"Unexpected result when disabling passkey: {passkey_result}") + + def _cleanup_local_credentials(self, rp_id: Optional[str] = None) -> bool: + """Clean up local biometric credentials (platform-specific)""" + try: + system = platform.system() + + if system == PLATFORM_DARWIN: # macOS + return self._cleanup_macos_keychain_credentials(rp_id) + elif system == PLATFORM_WINDOWS: # Windows + return self._cleanup_windows_credentials() + else: + return True + except Exception as e: + print(f"Warning: Could not clean up local credentials: {str(e)}") + return False + + def _cleanup_macos_keychain_credentials(self, rp_id: Optional[str] = None) -> bool: + """Clean up macOS keychain credentials for biometric authentication""" + try: + services_to_clean = ["Keeper Biometric Authentication"] + + if rp_id: + services_to_clean.append(f"{MACOS_KEYCHAIN_SERVICE_PREFIX} - {rp_id}") + else: + print("RP ID not available - performing limited cleanup") + + deleted_count = 0 + for service_name in services_to_clean: + try: + result = subprocess.run([ + 'security', 'find-internet-password', + '-s', service_name, + '-g' + ], capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + delete_result = subprocess.run([ + 'security', 'delete-internet-password', + '-s', service_name + ], capture_output=True, text=True, timeout=10) + + if delete_result.returncode == 0: + deleted_count += 1 + except Exception: + continue + + return True + except Exception: + return False + + def _cleanup_windows_credentials(self) -> bool: + """Clean up Windows credentials for biometric authentication""" + try: + return True + except Exception: + return False + + def _report_unregister_results(self, username: str, delete_success: bool, cleanup_success: bool): + """Report the results of the unregister operation""" + if delete_success: + print(SUCCESS_MESSAGES['unregistration_complete'] + f" for user '{username}'.") + if cleanup_success: + print("Default authentication will be used for future logins.") + else: + print(f"Failed to remove biometric authentication for user '{username}'. Please try again.") diff --git a/keepercli-package/src/keepercli/biometric/commands/update_name.py b/keepercli-package/src/keepercli/biometric/commands/update_name.py new file mode 100644 index 00000000..c353dd46 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/commands/update_name.py @@ -0,0 +1,150 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 32: + raise Exception("Friendly name must be 32 characters or less") + + if not self._confirm_update(target_credential, friendly_name): + print("Update cancelled by user") + return + + result = self.client.update_passkey_name(vault, target_credential['id'], target_credential['credential_id'], friendly_name) + + self._report_update_results(result, target_credential, friendly_name) + + return self._execute_with_error_handling('update passkey friendly name', _update_name) + + def _interactive_credential_selection(self, available_credentials): + """Interactive selection of credential to update""" + if len(available_credentials) == 1: + credential = available_credentials[0] + print(f"Found single credential: {credential['name']}") + answer = user_choice('Use this credential?', 'yn', 'y') + if answer.lower() == 'y': + return 0 # Return index instead of ID + else: + raise Exception("Operation cancelled by user") + + print("\nAvailable Biometric Credentials:") + print("-" * 50) + + for i, credential in enumerate(available_credentials, 1): + created_date = self._format_timestamp(credential.get('created')) + last_used = self._format_timestamp(credential.get('last_used')) + + print(f"{i:2}. {credential['name']}") + print(f" Created: {created_date}") + print(f" Last Used: {last_used}") + print() + + while True: + try: + choice = input(f"Select credential number (1-{len(available_credentials)}): ") + if choice.lower() in ['q', 'quit', 'exit']: + raise Exception("Operation cancelled by user") + + selection = int(choice) - 1 + if 0 <= selection < len(available_credentials): + selected_credential = available_credentials[selection] + print(f"Selected: {selected_credential['name']}") + return selection # Return index instead of ID + else: + print(f"Invalid selection. Please choose 1-{len(available_credentials)}.") + except ValueError: + print("Invalid input. Please enter a number.") + except KeyboardInterrupt: + raise Exception("Operation cancelled by user") + + def _interactive_name_input(self, credential): + """Interactive input for new friendly name""" + print(f"\nCurrent Name: {credential['name']}") + print("Enter a new friendly name (max 32 characters):") + + while True: + try: + new_name = input("New name: ").strip() + + if not new_name: + print("Name cannot be empty. Please try again.") + continue + + if len(new_name) > 32: + print(f"Name too long ({len(new_name)} chars). Maximum 32 characters allowed.") + continue + + if new_name == credential['name']: + print("Name is the same as current name. Please enter a different name.") + continue + + return new_name + + except KeyboardInterrupt: + raise Exception("Operation cancelled by user") + + def _confirm_update(self, credential, new_name): + """Confirm the update operation""" + print("\nUpdate Summary:") + print("-" * 20) + print(f"Current Name: {credential['name']}") + print(f"New Name: {new_name}") + print() + + answer = user_choice('Proceed with update?', 'yn', 'y') + return answer.lower() == 'y' + + def _report_update_results(self, result, credential, new_name): + """Report the update results to the user""" + print("\nPasskey Update Results:") + print("=" * 30) + status_code = result['status'] + status_text = get_status_message(status_code) + print(f"Status: {status_text}") + print(f"Old Name: {credential['name']}") + print(f"New Name: {new_name}") + print(f"Message: {result['message']}") + print("=" * 30) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/commands/verify.py b/keepercli-package/src/keepercli/biometric/commands/verify.py new file mode 100644 index 00000000..9756c2af --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/commands/verify.py @@ -0,0 +1,241 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Union[Dict[str, Any], APIRequest_pb2.PasskeyValidationResponse]: + """Verify the authentication response with Keeper""" + try: + actual_response = self._extract_assertion_response(assertion_response) + + if not hasattr(actual_response, 'response'): + raise ValueError(f"Invalid assertion response object: {type(actual_response)}") + + client_data_bytes = actual_response.response.client_data + client_data_b64 = self._extract_client_data_b64(client_data_bytes) + + if not actual_response.id or not actual_response.raw_id: + raise ValueError("Could not extract credential ID from assertion response") + + assertion_object = self._create_assertion_object(actual_response, client_data_b64) + + # Determine if this is a vault or login authentication + if isinstance(auth_context, vault_online.VaultOnline): + return self._send_verification_request( + auth_context, auth_options, assertion_object, purpose) + else: + return self._send_login_verification_request( + auth_context, auth_options, assertion_object, purpose) + + except (ValueError, AttributeError) as e: + raise ValueError(f"Authentication verification failed: {str(e)}") + except Exception as e: + raise RuntimeError(f"Unexpected error during authentication verification: {str(e)}") + + def _extract_client_data_b64(self, client_data_bytes: Any) -> str: + """Extract base64-encoded client data""" + if hasattr(client_data_bytes, 'b64'): + return client_data_bytes.b64 + elif isinstance(client_data_bytes, bytes): + return utils.base64_url_encode(client_data_bytes) + else: + return str(client_data_bytes) + + def _create_assertion_object(self, actual_response: Any, client_data_b64: str) -> Dict[str, Any]: + """Create assertion object for verification""" + return { + 'id': actual_response.id, + 'rawId': utils.base64_url_encode(actual_response.raw_id), + 'response': { + 'authenticatorData': utils.base64_url_encode(actual_response.response.authenticator_data), + 'clientDataJSON': client_data_b64, + 'signature': utils.base64_url_encode(actual_response.response.signature), + }, + 'type': 'public-key', + 'clientExtensionResults': getattr(actual_response, 'client_extension_results', {}) or {} + } + + def _send_verification_request( + self, + vault: vault_online.VaultOnline, + auth_options: Dict[str, Any], + assertion_object: Dict[str, Any], + purpose: str + ) -> Dict[str, Any]: + """Send verification request to Keeper API""" + + rq = APIRequest_pb2.PasskeyValidationRequest() + rq.challengeToken = auth_options['challenge_token'] + rq.assertionResponse = json.dumps(assertion_object).encode('utf-8') + rq.passkeyPurpose = (APIRequest_pb2.PasskeyPurpose.PK_REAUTH + if purpose == 'vault' else APIRequest_pb2.PasskeyPurpose.PK_LOGIN) + + if auth_options.get('login_token'): + login_token = auth_options['login_token'] + rq.encryptedLoginToken = utils.base64_url_decode(login_token) if isinstance(login_token, str) else login_token + + rs = vault.keeper_auth.execute_auth_rest(rest_endpoint=API_ENDPOINTS['verify_authentication'], request=rq, response_type=APIRequest_pb2.PasskeyValidationResponse) + + return { + 'is_valid': rs.isValid, + 'login_token': rs.encryptedLoginToken, + 'credential_id': assertion_object['id'].encode() if isinstance(assertion_object['id'], str) else assertion_object['id'], + 'user_handle': self._extract_user_handle(assertion_object) + } + + def _send_login_verification_request( + self, + login_auth: login_auth.LoginAuth, + auth_options: Dict[str, Any], + assertion_object: Dict[str, Any], + purpose: str + ) -> APIRequest_pb2.PasskeyValidationResponse: + """Send verification request to Keeper API""" + + rq = APIRequest_pb2.PasskeyValidationRequest() + rq.challengeToken = auth_options['challenge_token'] + rq.assertionResponse = json.dumps(assertion_object).encode('utf-8') + rq.passkeyPurpose = (APIRequest_pb2.PasskeyPurpose.PK_REAUTH + if purpose == 'vault' else APIRequest_pb2.PasskeyPurpose.PK_LOGIN) + + if auth_options.get('login_token'): + login_token = auth_options['login_token'] + rq.encryptedLoginToken = utils.base64_url_decode(login_token) if isinstance(login_token, str) else login_token + + rs = login_auth.execute_rest(rest_endpoint=API_ENDPOINTS['verify_authentication'], request=rq, response_type=APIRequest_pb2.PasskeyValidationResponse) + + return rs + + def _extract_user_handle(self, assertion_object: Dict[str, Any]) -> Optional[Any]: + """Extract user handle from assertion object safely""" + try: + response = assertion_object.get('response', {}) + if isinstance(response, dict): + return response.get('user_handle') + return None + except (AttributeError, KeyError): + return None + + def _extract_assertion_response(self, assertion_result: Any) -> Any: + """Extract assertion response from various result types""" + try: + if hasattr(assertion_result, 'get_response'): + response = assertion_result.get_response(0) + if response is None: + raise ValueError("No response found in assertion result") + return response + elif hasattr(assertion_result, 'get_assertions'): + assertions = assertion_result.get_assertions() + if not assertions: + raise ValueError("AssertionSelection has no assertions") + return assertions[0] + elif hasattr(assertion_result, 'response'): + return assertion_result + elif hasattr(assertion_result, 'assertions') and assertion_result.assertions: + return assertion_result.assertions[0] + else: + raise ValueError(f"Unknown assertion result format: {type(assertion_result)}") + except (ValueError, AttributeError) as e: + raise ValueError(f"Failed to extract assertion response: {str(e)}") + except Exception as e: + raise RuntimeError(f"Unexpected error extracting assertion response: {str(e)}") + + def _report_verification_results(self, verification_result: Dict[str, Any], purpose: str) -> None: + """Report the verification results to the user""" + logger = utils.get_logger() + logger.info(f"\nBiometric Authentication Verification Results:") + logger.info("=" * 50) + + if verification_result['is_valid']: + logger.info("Status: SUCCESSFUL") + logger.info(f"Purpose: {purpose.upper()}") + + if verification_result.get('user_handle'): + logger.info(f"User Handle: {utils.base64_url_encode(verification_result['user_handle'])}") + if verification_result.get('login_token'): + logger.info("Login Token: Received") + + logger.info(f"\n{SUCCESS_MESSAGES['verification_success']}") + else: + logger.info("Status: FAILED") + logger.info(f"Purpose: {purpose.upper()}") + logger.info("\n Authentication verification failed. Please check your biometric setup.") + + logger.info("=" * 50) + + def biometric_authenticate( + self, login_auth: login_auth.LoginAuth, client_version: str, username: str, + purpose: str = 'login', device_token: Optional[str] = None + ) -> Union[Dict[str, Any], APIRequest_pb2.PasskeyValidationResponse]: + """Perform biometric authentication for login""" + try: + auth_options = self.client.generate_login_authentication_options(login_auth, client_version, username, purpose, device_token) + assertion_response = self.client.perform_authentication(auth_options) + verification_result = self._verify_authentication_response(login_auth, auth_options, assertion_response, purpose) + + return verification_result + + except (ValueError, RuntimeError) as e: + raise RuntimeError(f"Biometric authentication failed: {str(e)}") + except Exception as e: + raise RuntimeError(f"Unexpected error during biometric authentication: {str(e)}") \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/__init__.py b/keepercli-package/src/keepercli/biometric/platforms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keepercli-package/src/keepercli/biometric/platforms/base.py b/keepercli-package/src/keepercli/biometric/platforms/base.py new file mode 100644 index 00000000..43a08e85 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/base.py @@ -0,0 +1,151 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Tuple[bool, str]: + """Detect biometric capabilities for this platform""" + pass + + @abstractmethod + def create_webauthn_client(self, data_collector): + """Create platform-specific WebAuthn client""" + pass + + @abstractmethod + def handle_credential_creation(self, creation_options: Dict[str, Any]) -> Dict[str, Any]: + """Handle platform-specific credential creation options""" + pass + + @abstractmethod + def handle_authentication_options(self, pk_options: Dict[str, Any]) -> Dict[str, Any]: + """Handle platform-specific authentication options""" + pass + + @abstractmethod + def perform_authentication(self, client, options: PublicKeyCredentialRequestOptions): + """Perform platform-specific authentication""" + pass + + @abstractmethod + def perform_credential_creation(self, client, options: PublicKeyCredentialCreationOptions): + """Perform platform-specific credential creation""" + pass + + +class StorageHandler(ABC): + """Abstract base class for biometric flag storage""" + + @abstractmethod + def get_biometric_flag(self, username: str) -> bool: + """Get biometric flag for user""" + pass + + @abstractmethod + def delete_biometric_flag(self, username: str) -> bool: + """Delete biometric flag for user""" + pass + + +class BasePlatformHandler(PlatformHandler): + """Base implementation for platform handlers with common functionality""" + + def __init__(self): + self.storage_handler = self._create_storage_handler() + + @abstractmethod + def _create_storage_handler(self) -> StorageHandler: + """Create platform-specific storage handler""" + pass + + def get_biometric_flag(self, username: str) -> bool: + """Get biometric flag for user""" + return self.storage_handler.get_biometric_flag(username) + + def delete_biometric_flag(self, username: str) -> bool: + """Delete biometric flag for user""" + return self.storage_handler.delete_biometric_flag(username) + + def _prepare_credential_creation_options(self, creation_options: Dict[str, Any]) -> Dict[str, Any]: + """Common credential creation options preparation""" + if isinstance(creation_options['user'].get('id'), str): + user_id = utils.base64_url_decode(creation_options['user']['id']) + creation_options['user']['id'] = user_id + + # Remove unsupported options + creation_options.pop('hints', None) + creation_options.pop('extensions', None) + + # Remove empty excludeCredentials + if 'excludeCredentials' in creation_options and not creation_options['excludeCredentials']: + creation_options.pop('excludeCredentials') + + # Set authenticator selection + if 'authenticatorSelection' not in creation_options: + creation_options['authenticatorSelection'] = {} + + # Apply default authenticator selection + default_selection = { + 'authenticatorAttachment': 'platform', + 'userVerification': 'required', + 'residentKey': 'required' + } + + creation_options['authenticatorSelection'].update(default_selection) + + if 'timeout' not in creation_options: + creation_options['timeout'] = DEFAULT_BIOMETRIC_TIMEOUT * 1000 + + return creation_options + + def _prepare_authentication_options(self, pk_options: Dict[str, Any]) -> Dict[str, Any]: + """Common authentication options preparation""" + pk_options.pop('hints', None) + pk_options.pop('extensions', None) + + # Clean up empty transports + if 'allowCredentials' in pk_options: + for cred in pk_options['allowCredentials']: + if 'transports' in cred and not cred['transports']: + cred.pop('transports') + + pk_options['userVerification'] = 'required' + + if 'timeout' not in pk_options: + pk_options['timeout'] = DEFAULT_BIOMETRIC_TIMEOUT * 1000 + + return pk_options + + def _handle_authentication_error(self, error: Exception, platform_name: str = "Biometric") -> Exception: + """Common error handling for authentication failures""" + from ..utils.error_handler import BiometricErrorHandler + return BiometricErrorHandler.handle_authentication_error(error, platform_name) + + def _handle_credential_creation_error(self, error: Exception, platform_name: str = "Biometric") -> Exception: + """Common error handling for credential creation failures""" + from ..utils.error_handler import BiometricErrorHandler + return BiometricErrorHandler.handle_credential_creation_error(error, platform_name) + + @abstractmethod + def _get_platform_name(self) -> str: + """Get platform-specific name for error messages""" + pass \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/detector.py b/keepercli-package/src/keepercli/biometric/platforms/detector.py new file mode 100644 index 00000000..ffdcf797 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/detector.py @@ -0,0 +1,63 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' dict: + """Load available platform handlers""" + handlers = {} + + try: + if platform.system() == PLATFORM_WINDOWS: + from .windows import WindowsHandler + handlers[PLATFORM_WINDOWS] = WindowsHandler() + except ImportError: + logging.debug("Windows platform handler not available") + + try: + if platform.system() == PLATFORM_DARWIN: + from .macos import MacOSHandler + handlers[PLATFORM_DARWIN] = MacOSHandler() + except ImportError: + logging.debug("macOS platform handler not available") + + return handlers + + def detect_platform_capabilities(self) -> Tuple[bool, str]: + """Detect biometric capabilities for current platform""" + current_platform = platform.system() + + if current_platform not in self._platform_handlers: + return False, f"Biometric authentication not supported on {current_platform}" + + handler = self._platform_handlers[current_platform] + return handler.detect_capabilities() + + def get_platform_handler(self) -> PlatformHandler: + """Get platform handler for current system""" + current_platform = platform.system() + + if current_platform not in self._platform_handlers: + raise Exception(f"Biometric authentication not supported on {current_platform}") + + return self._platform_handlers[current_platform] \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/macos/__init__.py b/keepercli-package/src/keepercli/biometric/platforms/macos/__init__.py new file mode 100644 index 00000000..7697cd03 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/macos/__init__.py @@ -0,0 +1,3 @@ +from .handler import MacOSHandler + +__all__ = ['MacOSHandler'] \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/macos/handler.py b/keepercli-package/src/keepercli/biometric/platforms/macos/handler.py new file mode 100644 index 00000000..f66ceb10 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/macos/handler.py @@ -0,0 +1,260 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' bool: + """Get biometric flag from macOS preferences - True if credential ID exists""" + return self.get_credential_id(username) is not None + + def delete_biometric_flag(self, username: str) -> bool: + """Delete biometric flag from macOS preferences - removes credential ID""" + return self.delete_credential_id(username) + + def store_credential_id(self, username: str, credential_id: str) -> bool: + """Store credential ID for user in macOS preferences (also serves as biometric flag)""" + try: + import plistlib + prefs = {} + + if os.path.exists(self.prefs_path): + try: + with open(self.prefs_path, 'rb') as f: + prefs = plistlib.load(f) + except Exception: + prefs = {} + + prefs[username] = credential_id + + os.makedirs(os.path.dirname(self.prefs_path), exist_ok=True) + with open(self.prefs_path, 'wb') as f: + plistlib.dump(prefs, f) + + logging.debug("Stored credential ID for user: %s", username) + return True + except Exception as e: + logging.warning("Failed to store credential ID for %s: %s", username, str(e)) + BiometricErrorHandler.create_storage_error("store credential ID", "macOS", e) + return False + + def get_credential_id(self, username: str) -> Optional[str]: + """Get stored credential ID for user from macOS preferences""" + try: + import plistlib + if not os.path.exists(self.prefs_path): + return None + + with open(self.prefs_path, 'rb') as f: + prefs = plistlib.load(f) + + value = prefs.get(username) + + if value: + if isinstance(value, str): + return value + + return None + except Exception as e: + logging.warning("Failed to retrieve credential ID for %s: %s", username, str(e)) + BiometricErrorHandler.create_storage_error("get credential ID", "macOS", e) + return None + + def delete_credential_id(self, username: str) -> bool: + """Delete stored credential ID for user from macOS preferences""" + try: + import plistlib + if not os.path.exists(self.prefs_path): + return True + + with open(self.prefs_path, 'rb') as f: + prefs = plistlib.load(f) + + if username not in prefs: + return True + + del prefs[username] + + with open(self.prefs_path, 'wb') as f: + plistlib.dump(prefs, f) + + with open(self.prefs_path, 'rb') as f: + verification_prefs = plistlib.load(f) + + return username not in verification_prefs + except Exception as e: + logging.warning("Failed to delete credential ID for %s: %s", username, str(e)) + BiometricErrorHandler.create_storage_error("delete credential ID", "macOS", e) + return False + +class MacOSHandler(BasePlatformHandler): + """macOS-specific biometric handler""" + + def __init__(self): + super().__init__() + self.keychain_manager = MacOSKeychainManager() + + def _create_storage_handler(self) -> StorageHandler: + return MacOSStorageHandler() + + def _get_platform_name(self) -> str: + return "Touch ID" + + def detect_capabilities(self) -> Tuple[bool, str]: + """Detect Touch ID availability on macOS""" + if platform.system() != PLATFORM_DARWIN: + return False, "Not running on macOS" + + error_messages = [] + + try: + # Try bioutil command first + if self._check_bioutil_command(error_messages): + return True, "Touch ID is available and configured" + + # Fallback: LocalAuthentication check + if self._check_local_authentication(error_messages): + return True, "Touch ID is available" + + # If we get here, all detection methods failed + detailed_error = "Touch ID detection failed. " + "; ".join(error_messages) + detailed_error += ". Please verify Touch ID is set up in System Preferences > Touch ID & Password" + return False, detailed_error + + except Exception as e: + return False, f"Error checking Touch ID: {str(e)}" + + def _check_bioutil_command(self, error_messages: list) -> bool: + """Check Touch ID using bioutil command""" + try: + result = subprocess.run(MACOS_BIOUTIL_COMMAND, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + output = result.stdout.lower() + if ('touch id' in output or + 'biometrics functionality: 1' in output or + 'biometric' in output): + return True + else: + error_messages.append("bioutil: ran successfully but no Touch ID detected") + else: + error_messages.append(f"bioutil: command failed (return code {result.returncode})") + except FileNotFoundError: + error_messages.append("bioutil: command not found") + except Exception as e: + error_messages.append(f"bioutil: {str(e)}") + + return False + + def _check_local_authentication(self, error_messages: list) -> bool: + """Check Touch ID using LocalAuthentication framework""" + try: + import LocalAuthentication # pylint: disable=import-error + context = LocalAuthentication.LAContext.alloc().init() # pylint: disable=no-member + error = None + + policy_attr = getattr(LocalAuthentication, 'LAPolicyDeviceOwnerAuthenticationWithBiometrics', None) + if policy_attr is None: + error_messages.append("LocalAuthentication: biometric policy not available") + return False + + can_evaluate = context.canEvaluatePolicy_error_(policy_attr, error) + + if can_evaluate: + return True + else: + la_error = "LocalAuthentication: policy evaluation failed" + if error: + la_error += f" (error: {error})" + error_messages.append(la_error) + + except ImportError as e: + error_messages.append(f"LocalAuthentication: import failed - {str(e)}") + error_messages.append("LocalAuthentication: try 'pip install pyobjc-framework-LocalAuthentication'") + except Exception as e: + error_messages.append(f"LocalAuthentication: {str(e)}") + + return False + + def create_webauthn_client(self, data_collector): + """Create macOS Touch ID WebAuthn client""" + try: + return MacOSTouchIDWebAuthnClient(data_collector, self.keychain_manager) + except ImportError: + raise Exception('macOS Touch ID client dependencies not available') + + def handle_credential_creation(self, creation_options: Dict[str, Any]) -> Dict[str, Any]: + """Handle macOS-specific credential creation""" + rp_id = creation_options.get('rp', {}).get('id') + if not rp_id: + raise Exception("No RP ID found in creation options - server configuration error") + + # Check for existing credentials before processing + if 'excludeCredentials' in creation_options and creation_options['excludeCredentials']: + for excluded_cred in creation_options['excludeCredentials']: + cred_id = excluded_cred.get('id') + if isinstance(cred_id, str): + cred_id_b64 = cred_id + else: + cred_id_b64 = utils.base64_url_encode(cred_id) + + if self.keychain_manager.credential_exists(cred_id_b64, rp_id, DEFAULT_BIOMETRIC_TIMEOUT): + raise Exception(ERROR_MESSAGES['credential_exists']) + + return self._prepare_credential_creation_options(creation_options) + + def handle_authentication_options(self, pk_options: Dict[str, Any]) -> Dict[str, Any]: + """Handle macOS-specific authentication options""" + return self._prepare_authentication_options(pk_options) + + def perform_authentication(self, client, options: PublicKeyCredentialRequestOptions): + """Perform macOS Touch ID authentication""" + try: + return client.get_assertion(options) + except Exception as e: + raise self._handle_authentication_error(e, self._get_platform_name()) + + def perform_credential_creation(self, client, options: PublicKeyCredentialCreationOptions): + """Perform macOS Touch ID credential creation""" + try: + return client.make_credential(options) + except Exception as e: + raise self._handle_credential_creation_error(e, self._get_platform_name()) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/macos/keychain.py b/keepercli-package/src/keepercli/biometric/platforms/macos/keychain.py new file mode 100644 index 00000000..8b193ca9 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/macos/keychain.py @@ -0,0 +1,209 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Optional[str]: + """Authenticate with Touch ID and return the credential data""" + try: + timeout = timeout_seconds or DEFAULT_BIOMETRIC_TIMEOUT + return self._access_keychain_item(service_name, account_name, timeout) + except Exception as e: + logging.debug(f"Touch ID authentication failed: {str(e)}") + return None + + def _access_keychain_item(self, service_name: str, account_name: str, timeout_seconds: float) -> Optional[str]: + """Access keychain item with authentication""" + try: + result = subprocess.run([ + 'security', 'find-internet-password', + '-s', service_name, + '-a', account_name, + '-w' + ], capture_output=True, text=True, timeout=timeout_seconds) + + return result.stdout.strip() if result.returncode == 0 else None + except Exception as e: + logging.debug(f"Keychain access failed: {str(e)}") + return None + + def store_credential(self, credential_id: str, private_key_data: bytes, rp_id: str, timeout_seconds: Optional[float] = None) -> bool: + """Store private key in macOS keychain with Touch ID access control""" + try: + timeout = timeout_seconds or DEFAULT_BIOMETRIC_TIMEOUT + encoded_key = base64.b64encode(private_key_data).decode('ascii') + service_name = f"{self.service_prefix} - {rp_id}" + account_name = f"webauthn-{credential_id}" + + success = self._store_with_touchid_access(service_name, account_name, encoded_key, rp_id, timeout) + + if success: + self._set_touchid_access_control(service_name, account_name, timeout) + + return success + + except Exception as e: + BiometricErrorHandler.create_storage_error("store", "macOS keychain", e) + return False + + def _store_with_touchid_access(self, service_name: str, account_name: str, + encoded_key: str, rp_id: str, timeout_seconds: float) -> bool: + """Store credential with Touch ID access control""" + try: + result = subprocess.run([ + 'security', 'add-internet-password', + '-s', service_name, + '-a', account_name, + '-w', encoded_key, + '-D', 'WebAuthn Credential', + '-j', f'Keeper biometric credential for {rp_id}', + '-A', # Allow access from any application + '-T', '', # No specific application restrictions + '-U' # Update if exists + ], capture_output=True, text=True, timeout=timeout_seconds) + + if result.returncode == 0: + return True + + return False + + except Exception as e: + logging.warning(f"Failed to store credential: {str(e)}") + return False + + def _set_touchid_access_control(self, service_name: str, account_name: str, timeout_seconds: float): + """Set Touch ID access control for stored credential""" + try: + subprocess.run([ + 'security', 'set-internet-password-partition-list', + '-s', service_name, + '-a', account_name, + '-S', 'SmartCard,TouchID', + '-k', '' + ], capture_output=True, text=True, timeout=timeout_seconds) + + except Exception as e: + logging.debug(f"Could not set Touch ID access control: {str(e)}") + + def load_credential(self, credential_id: str, rp_id: Optional[str] = None, timeout_seconds: Optional[float] = None) -> Optional[object]: + """Load private key from macOS keychain using Touch ID""" + try: + timeout = timeout_seconds or DEFAULT_BIOMETRIC_TIMEOUT + account_name = f"webauthn-{credential_id}" + if not rp_id: + raise Exception("RP ID is required for credential loading") + service_names = [f"{self.service_prefix} - {rp_id}"] + + for service_name in service_names: + encoded_key = self._load_from_service(service_name, account_name, timeout) + if encoded_key: + key_data = base64.b64decode(encoded_key) + return crypto.load_ec_private_key(key_data) + + return None + + except Exception as e: + BiometricErrorHandler.create_storage_error("load", "macOS keychain", e) + return None + + def _load_from_service(self, service_name: str, account_name: str, timeout_seconds: float) -> Optional[str]: + """Load credential from specific service""" + try: + result = subprocess.run([ + 'security', 'find-internet-password', + '-s', service_name, + '-a', account_name, + '-w' + ], capture_output=True, text=True, timeout=timeout_seconds) + + if result.returncode == 0: + return result.stdout.strip() + + if result.returncode == 44: + return self._authenticate_with_touchid(service_name, account_name, timeout_seconds) + + except Exception: + pass + + return None + + def delete_credential(self, credential_id: str, rp_id: Optional[str] = None, timeout_seconds: Optional[float] = None) -> bool: + """Delete private key from macOS keychain""" + try: + timeout = timeout_seconds or DEFAULT_BIOMETRIC_TIMEOUT + account_name = f"webauthn-{credential_id}" + if not rp_id: + raise Exception("RP ID is required for credential deletion") + service_names = [f"{self.service_prefix} - {rp_id}"] + + success = False + for service_name in service_names: + try: + result = subprocess.run([ + 'security', 'delete-internet-password', + '-s', service_name, + '-a', account_name + ], capture_output=True, text=True, timeout=timeout) + + if result.returncode == 0: + success = True + + except Exception: + continue + + return success + + except Exception as e: + BiometricErrorHandler.create_storage_error("delete", "macOS keychain", e) + return False + + def credential_exists(self, credential_id: str, rp_id: Optional[str] = None, timeout_seconds: Optional[float] = None) -> bool: + """Check if credential exists in macOS keychain""" + try: + timeout = timeout_seconds or DEFAULT_BIOMETRIC_TIMEOUT + account_name = f"webauthn-{credential_id}" + if not rp_id: + raise Exception("RP ID is required for credential existence check") + service_names = [f"{self.service_prefix} - {rp_id}"] + + for service_name in service_names: + try: + result = subprocess.run([ + 'security', 'find-internet-password', + '-s', service_name, + '-a', account_name, + '-w' + ], capture_output=True, text=True, timeout=timeout) + + if result.returncode == 0: + return True + + except Exception: + continue + + return False + + except Exception: + return False \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/macos/webauthn.py b/keepercli-package/src/keepercli/biometric/platforms/macos/webauthn.py new file mode 100644 index 00000000..e69006f3 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/macos/webauthn.py @@ -0,0 +1,368 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' None: + """Validate that required modules are available""" + BiometricErrorHandler.validate_dependencies(required_modules) + + def _create_client_data(self, data_type: str, challenge: bytes, rp_id: str) -> Tuple[Dict[str, Any], bytes]: + """Create WebAuthn client data""" + origin = getattr(self.client_data_collector, 'origin', f'https://{rp_id}') + client_data = { + 'type': data_type, + 'challenge': utils.base64_url_encode(challenge), + 'origin': origin, + 'crossOrigin': False + } + client_data_json = json.dumps(client_data, separators=(',', ':')).encode() + return client_data, client_data_json + + def _create_authenticator_data(self, rp_id: str, flags: int, counter: int = 0, + additional_data: bytes = b'') -> bytes: + """Create WebAuthn authenticator data""" + import hashlib + import struct + + rp_id_hash = hashlib.sha256(rp_id.encode()).digest() + counter_bytes = struct.pack('>I', counter) + return rp_id_hash + struct.pack('B', flags) + counter_bytes + additional_data + + def _create_signed_data(self, authenticator_data: bytes, client_data_json: bytes) -> bytes: + """Create data to be signed for WebAuthn""" + import hashlib + client_data_hash = hashlib.sha256(client_data_json).digest() + return authenticator_data + client_data_hash + + +class MacOSTouchIDWebAuthnClient(BaseWebAuthnClient): + """macOS Touch ID WebAuthn client with DRY principles""" + + def __init__(self, client_data_collector, keychain_manager): + super().__init__(client_data_collector, keychain_manager) + + def make_credential(self, options): + """Create WebAuthn credential using Touch ID""" + try: + self._validate_dependencies(['LocalAuthentication', 'cbor2']) + + timeout_ms = getattr(options, 'timeout', None) + timeout_seconds = (timeout_ms / 1000.0) if timeout_ms else DEFAULT_BIOMETRIC_TIMEOUT + + if hasattr(options, 'exclude_credentials') and options.exclude_credentials: + for excluded_cred in options.exclude_credentials: + cred_id = excluded_cred.id + if isinstance(cred_id, str): + cred_id_b64 = cred_id + else: + cred_id_b64 = utils.base64_url_encode(cred_id) + + if self.keychain_manager.credential_exists(cred_id_b64, options.rp.id, timeout_seconds): + raise OSError("The object already exists") + + credential_id = utils.base64_url_encode(crypto.get_random_bytes(32)) + private_key, public_key = crypto.generate_ec_key() + public_key_data = crypto.unload_ec_public_key(public_key) + private_key_data = crypto.unload_ec_private_key(private_key) + + rp_id = options.rp.id + if not rp_id: + raise Exception("No RP ID found in options - server configuration error") + context = self._create_auth_context() + self._check_biometric_availability(context) + + if not self.keychain_manager.store_credential(credential_id, private_key_data, rp_id, timeout_seconds): + raise Exception(ERROR_MESSAGES['keychain_store_failed']) + + reason = AUTH_REASONS['register'].format(rp_id=rp_id) + success = self._authenticate_with_touchid(context, reason, timeout_seconds) + + if not success: + self.keychain_manager.delete_credential(credential_id, rp_id, timeout_seconds) + raise Exception(ERROR_MESSAGES['authentication_failed']) + + return self._create_credential_response( + options, credential_id, public_key_data, rp_id + ) + + except Exception as e: + raise Exception(str(e)) + + def get_assertion(self, options): + """Get WebAuthn assertion using Touch ID""" + try: + self._validate_dependencies(['LocalAuthentication']) + + timeout_ms = getattr(options, 'timeout', None) + timeout_seconds = (timeout_ms / 1000.0) if timeout_ms else DEFAULT_BIOMETRIC_TIMEOUT + + challenge = options.challenge + rp_id = options.rp_id + if not rp_id: + raise Exception("No RP ID found in options - server configuration error") + private_key, credential_id_b64, credential_id_bytes = self._find_credential(options, rp_id, timeout_seconds) + + context = self._create_auth_context() + self._check_biometric_availability(context) + + reason = AUTH_REASONS['login'].format(rp_id=rp_id) + success = self._authenticate_with_touchid(context, reason, timeout_seconds) + + if not success: + raise Exception(ERROR_MESSAGES['authentication_failed']) + + return self._create_assertion_response( + challenge, rp_id, credential_id_b64, credential_id_bytes, private_key + ) + + except Exception as e: + raise Exception(str(e)) + + def _create_auth_context(self): + """Create LocalAuthentication context""" + import LocalAuthentication # pylint: disable=import-error + return LocalAuthentication.LAContext.alloc().init() # pylint: disable=no-member + + def _check_biometric_availability(self, context): + """Check if biometric authentication is available""" + import LocalAuthentication # pylint: disable=import-error + + error = None + can_evaluate = context.canEvaluatePolicy_error_( + LocalAuthentication.LAPolicyDeviceOwnerAuthenticationWithBiometrics, # pylint: disable=no-member + error + ) + + if not can_evaluate: + raise Exception(ERROR_MESSAGES['touchid_not_available']) + + def _authenticate_with_touchid(self, context, reason: str, timeout_seconds: float = 30) -> bool: + """Perform Touch ID authentication with proper synchronization""" + import LocalAuthentication # pylint: disable=import-error + import threading + + event = threading.Event() + result = {'success': False, 'error': None} + + def auth_callback(success, error): + result['success'] = bool(success) + result['error'] = error + event.set() + + context.evaluatePolicy_localizedReason_reply_( + LocalAuthentication.LAPolicyDeviceOwnerAuthenticationWithBiometrics, # pylint: disable=no-member + reason, + auth_callback + ) + + # Wait for result with synchronization + if event.wait(timeout=timeout_seconds): + if result['error']: + from ...utils.error_handler import BiometricErrorHandler + raise BiometricErrorHandler.handle_authentication_error( + Exception(str(result['error'])), "Touch ID" + ) + return result['success'] + else: + raise TimeoutError("Touch ID authentication timed out") + + def _find_credential(self, options, rp_id: str, timeout_seconds: float) -> Tuple[Any, str, bytes]: + """Find credential using keychain manager""" + allowed_credentials = options.allow_credentials or [] + if not allowed_credentials: + raise Exception("No allowed credentials found") + + for cred in allowed_credentials: + cred_id = cred.id + if isinstance(cred_id, str): + test_id_b64 = cred_id + test_id_bytes = utils.base64_url_decode(cred_id) + else: + test_id_bytes = cred_id + test_id_b64 = utils.base64_url_encode(cred_id) + + test_key = self.keychain_manager.load_credential(test_id_b64, rp_id, timeout_seconds) + if test_key: + return test_key, test_id_b64, test_id_bytes + + from ...utils.error_handler import BiometricErrorHandler + raise BiometricErrorHandler.handle_authentication_error( + Exception("no matching credential found"), "Touch ID" + ) + + def _create_credential_response(self, options, credential_id: str, + public_key_data: bytes, rp_id: str): + """Create WebAuthn credential response""" + import cbor2 + import struct + + challenge = options.challenge + client_data, client_data_json = self._create_client_data( + WEBAUTHN_CHALLENGE_TYPE_CREATE, challenge, rp_id + ) + + # Create COSE key + x_coord, y_coord = self._extract_key_coordinates(public_key_data) + cose_key = { + 1: EC_KTY_EC2, # kty: EC2 + 3: EC_ALG_ES256, # alg: ES256 + -1: EC_CURVE_P256, # crv: P-256 + -2: x_coord, + -3: y_coord + } + + # Create attested credential data + attested_credential_data = ( + b'\x00' * 16 + # AAGUID + struct.pack('>H', len(utils.base64_url_decode(credential_id))) + + utils.base64_url_decode(credential_id) + + cbor2.dumps(cose_key) + ) + + # Create authenticator data + authenticator_data = self._create_authenticator_data( + rp_id, AUTH_FLAG_CREATION, 0, attested_credential_data + ) + + # Create attestation object + attestation_object = { + 'fmt': 'none', + 'attStmt': {}, + 'authData': authenticator_data + } + + return self._create_registration_response( + credential_id, + client_data_json, + cbor2.dumps(attestation_object) + ) + + def _create_assertion_response(self, challenge: bytes, rp_id: str, + credential_id_b64: str, credential_id_bytes: bytes, + private_key): + """Create WebAuthn assertion response""" + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import ec + + client_data, client_data_json = self._create_client_data( + WEBAUTHN_CHALLENGE_TYPE_GET, challenge, rp_id + ) + + # Create authenticator data + authenticator_data = self._create_authenticator_data(rp_id, AUTH_FLAG_ASSERTION) + + # Sign the data + signed_data = self._create_signed_data(authenticator_data, client_data_json) + der_signature = private_key.sign(signed_data, ec.ECDSA(hashes.SHA256())) + + return self._create_authentication_response( + credential_id_b64, + credential_id_bytes, + client_data_json, + authenticator_data, + der_signature + ) + + def _extract_key_coordinates(self, public_key_data: bytes) -> Tuple[bytes, bytes]: + """Extract x and y coordinates from EC key""" + expected_length = 1 + (2 * EC_COORDINATE_LENGTH) # 1 + (2 * 32) = 65 + if len(public_key_data) != expected_length or public_key_data[0] != EC_PUBLIC_KEY_UNCOMPRESSED_PREFIX: + raise Exception("Invalid P-256 public key format") + + return (public_key_data[1:1 + EC_COORDINATE_LENGTH], + public_key_data[1 + EC_COORDINATE_LENGTH:]) + + def _create_registration_response(self, credential_id: str, client_data_json: bytes, + attestation_object_cbor: bytes): + """Create registration response""" + class RegistrationResponse: + def __init__(self, cred_id, cred_raw_id, client_data, attestation_obj): + self.id = cred_id + self.raw_id = cred_raw_id + self.response = AttestationResponse(client_data, attestation_obj) + self.client_extension_results = {} + self.type = WEBAUTHN_CREDENTIAL_TYPE + + class AttestationResponse: + def __init__(self, client_data, attestation_obj): + self.client_data = client_data + self.attestation_object = attestation_obj + + return RegistrationResponse( + credential_id, + utils.base64_url_decode(credential_id), + client_data_json, + attestation_object_cbor + ) + + def _create_authentication_response(self, credential_id_b64: str, credential_id_bytes: bytes, + client_data_json: bytes, authenticator_data: bytes, + signature: bytes): + """Create authentication response""" + class AuthenticationResponse: + def __init__(self, cred_id, cred_raw_id, client_data, auth_data, sig): + self.id = cred_id + self.raw_id = cred_raw_id + self.response = AssertionResponse(client_data, auth_data, sig) + self.client_extension_results = {} + self.type = WEBAUTHN_CREDENTIAL_TYPE + + class AssertionResponse: + def __init__(self, client_data, auth_data, sig): + self.client_data = client_data + self.authenticator_data = auth_data + self.signature = sig + self.user_handle = None + + return AuthenticationResponse( + credential_id_b64, + credential_id_bytes, + client_data_json, + authenticator_data, + signature + ) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/platforms/windows.py b/keepercli-package/src/keepercli/biometric/platforms/windows.py new file mode 100644 index 00000000..68ed1e0b --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/platforms/windows.py @@ -0,0 +1,246 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Optional[Any]: + """Get Windows registry key""" + try: + import winreg + try: + return winreg.OpenKey(winreg.HKEY_CURRENT_USER, self.key_path, 0, winreg.KEY_ALL_ACCESS) + except FileNotFoundError: + return winreg.CreateKey(winreg.HKEY_CURRENT_USER, self.key_path) + except ImportError: + return None + + @contextmanager + def registry_key(self) -> Any: + """Context manager for registry key handling""" + import winreg + key = None + try: + key = self._get_registry_key() + if key is None: + raise ImportError("winreg module not available") + yield key + finally: + if key: + winreg.CloseKey(key) + + def get_biometric_flag(self, username: str) -> bool: + """Get biometric flag from Windows registry - True if credential ID exists""" + return self.get_credential_id(username) is not None + + def delete_biometric_flag(self, username: str) -> bool: + """Delete biometric flag from Windows registry - removes credential ID""" + return self.delete_credential_id(username) + + def store_credential_id(self, username: str, credential_id: str) -> bool: + """Store credential ID for user in Windows registry (also serves as biometric flag)""" + try: + import winreg + with self.registry_key() as key: + winreg.SetValueEx(key, username, 0, winreg.REG_SZ, credential_id) + logging.debug("Stored credential ID for user: %s", username) + return True + except Exception as e: + logging.warning("Failed to store credential ID for %s: %s", username, str(e)) + BiometricErrorHandler.create_storage_error("store credential ID", "Windows registry", e) + return False + + def get_credential_id(self, username: str) -> Optional[str]: + """Get stored credential ID for user from Windows registry""" + try: + import winreg + with self.registry_key() as key: + try: + value, reg_type = winreg.QueryValueEx(key, username) + if reg_type == winreg.REG_SZ and value: + logging.debug("Retrieved credential ID for user: %s", username) + return str(value) + else: + return None + except FileNotFoundError: + logging.debug("No stored credential ID found for user: %s", username) + return None + except Exception as e: + logging.warning("Failed to retrieve credential ID for %s: %s", username, str(e)) + BiometricErrorHandler.create_storage_error("get credential ID", "Windows registry", e) + return None + + def delete_credential_id(self, username: str) -> bool: + """Delete stored credential ID for user from Windows registry""" + try: + import winreg + with self.registry_key() as key: + try: + winreg.DeleteValue(key, username) + logging.debug("Deleted stored credential ID for user: %s", username) + return True + except FileNotFoundError: + logging.debug("Credential ID for user %s was already deleted", username) + return True + except Exception as e: + logging.warning("Failed to delete credential ID for %s: %s", username, str(e)) + BiometricErrorHandler.create_storage_error("delete credential ID", "Windows registry", e) + return False + + +class WindowsHandler(BasePlatformHandler): + """Windows-specific biometric handler""" + + def _create_storage_handler(self) -> StorageHandler: + return WindowsStorageHandler() + + def _get_platform_name(self) -> str: + return "Windows Hello" + + def _get_current_user_sid(self) -> Optional[str]: + """Get current user SID using PowerShell and WMI""" + try: + cmd = ['powershell', '-Command', + f"(Get-WmiObject -Class Win32_UserAccount -Filter \"Name='{getpass.getuser()}'\").SID"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout.strip() + except subprocess.CalledProcessError: + try: + cmd = ['whoami', '/user', '/fo', 'csv'] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + lines = result.stdout.strip().split('\n') + if len(lines) > 1: + sid_line = lines[1].split(',') + if len(sid_line) > 1: + return sid_line[1].strip('"') + except subprocess.CalledProcessError: + pass + + return None + + + async def _check_biometrics(self) -> bool: + """Check if biometrics (face/fingerprint) are enrolled using Windows Runtime API""" + try: + availability = await UserConsentVerifier.check_availability_async() + + if availability == UserConsentVerifierAvailability.AVAILABLE: + return True + else: + return False + except Exception as e: + logging.debug("Failed to check biometrics availability: %s", str(e)) + return False + + # def _check_biometrics(self) -> bool: + # """Check if biometrics (face/fingerprint) are enrolled""" + # sid = self._get_current_user_sid() + # if not sid: + # return False + + # reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\WinBio\AccountInfo\{}".format(sid) + # try: + # import winreg + # with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: + # value, regtype = winreg.QueryValueEx(key, "EnrolledFactors") + # # 2 = Face, 8 = Fingerprint, 10 = Face and Fingerprint + # return value in [2, 8, 10] + # except (FileNotFoundError, ImportError): + # return False + + # def _check_pin_enrollment(self) -> bool: + # """Check if PIN is enrolled""" + # sid = self._get_current_user_sid() + # if not sid: + # return False + + # reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{{D6886603-9D2F-4EB2-B667-1971041FA96B}}\{}".format(sid) + # try: + # import winreg + # with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: + # value, regtype = winreg.QueryValueEx(key, "LogonCredsAvailable") + # return value == 1 + # except (FileNotFoundError, ImportError): + # return False + + def detect_capabilities(self) -> Tuple[bool, str]: + """Detect Windows Hello capabilities""" + if os.name != 'nt': + return False, "Not running on Windows" + + try: + # Run the async biometrics check + has_biometrics = asyncio.run(self._check_biometrics()) + + if has_biometrics: + return True, "Windows Hello available: Biometrics" + else: + return False, ERROR_MESSAGES['windows_hello_not_setup'] + + except Exception as e: + logging.warning("Error detecting Windows Hello: %s", str(e)) + return False, f"Error detecting Windows Hello: {str(e)}" + + def create_webauthn_client(self, data_collector): + """Create Windows WebAuthn client""" + try: + from fido2.client.windows import WindowsClient + return WindowsClient(client_data_collector=data_collector) + except ImportError: + raise Exception('Windows Hello client not available. Install fido2[pcsc]') + + def handle_credential_creation(self, creation_options: Dict[str, Any]) -> Dict[str, Any]: + """Handle Windows-specific credential creation""" + return self._prepare_credential_creation_options(creation_options) + + def handle_authentication_options(self, pk_options: Dict[str, Any]) -> Dict[str, Any]: + """Handle Windows-specific authentication options""" + return self._prepare_authentication_options(pk_options) + + def perform_authentication(self, client, options: PublicKeyCredentialRequestOptions): + """Perform Windows Hello authentication""" + try: + return client.get_assertion(options) + except Exception as e: + raise self._handle_authentication_error(e, self._get_platform_name()) + + def perform_credential_creation(self, client, options: PublicKeyCredentialCreationOptions): + """Perform Windows Hello credential creation""" + try: + return client.make_credential(options) + except Exception as e: + raise self._handle_credential_creation_error(e, self._get_platform_name()) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/utils/__init__.py b/keepercli-package/src/keepercli/biometric/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keepercli-package/src/keepercli/biometric/utils/aaguid.py b/keepercli-package/src/keepercli/biometric/utils/aaguid.py new file mode 100644 index 00000000..9445beaf --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/utils/aaguid.py @@ -0,0 +1,41 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' str: + """Get friendly provider name from AAGUID""" + if not aaguid: + return None + + # Normalize AAGUID format (ensure lowercase, with dashes) + normalized_aaguid = aaguid.lower() + if len(normalized_aaguid) == 32: # No dashes + normalized_aaguid = f"{normalized_aaguid[:8]}-{normalized_aaguid[8:12]}-{normalized_aaguid[12:16]}-{normalized_aaguid[16:20]}-{normalized_aaguid[20:]}" + + return AAGUID_PROVIDER_MAPPING.get(normalized_aaguid) diff --git a/keepercli-package/src/keepercli/biometric/utils/constants.py b/keepercli-package/src/keepercli/biometric/utils/constants.py new file mode 100644 index 00000000..e882b7c1 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/utils/constants.py @@ -0,0 +1,121 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' str: + """Get readable message for HTTP status code""" + return STATUS_MESSAGES.get(status_code, f"Unknown Status ({status_code})") + +def is_success_status(status_code: int) -> bool: + """Check if status code indicates success (2xx range)""" + return 200 <= status_code < 300 + +# API Endpoints +API_ENDPOINTS = { + 'generate_registration': 'authentication/passkey/generate_registration', + 'verify_registration': 'authentication/passkey/verify_registration', + 'generate_authentication': 'authentication/passkey/generate_authentication', + 'verify_authentication': 'authentication/passkey/verify_authentication', + 'get_available_keys': 'authentication/passkey/get_available_keys', + 'disable_passkey': 'authentication/passkey/disable', + 'update_passkey_name': 'authentication/passkey/update_friendly_name' +} + +# API Response Messages +API_RESPONSE_MESSAGES = { + 'passkey_disabled_success': 'Passkey was successfully disabled and no longer available for login', + 'passkey_name_updated_success': 'Passkey friendly name was successfully updated', + 'disable_bad_request': 'Unable to disable. Data error. Credential ID or UserID mismatch', + 'update_bad_request': 'Unable to update. Data error. Credential ID or UserID mismatch', + 'server_exception': 'Unexpected server exception' +} + +AUTHENTICATOR_SELECTION = { + 'authenticatorAttachment': 'platform', + 'userVerification': 'required' +} + +# Storage paths and service names +WINDOWS_REGISTRY_PATH = r"SOFTWARE\Keeper Security\Commander\Biometric" +MACOS_PREFS_PATH = "com.keepersecurity.commander.biometric.plist" +MACOS_KEYCHAIN_SERVICE_PREFIX = "Keeper WebAuthn" + +# FIDO2 availability check +try: + from fido2.client import ClientError, DefaultClientDataCollector, UserInteraction, WebAuthnClient + from fido2.ctap import CtapError + from fido2.webauthn import ( + PublicKeyCredentialRequestOptions, + AuthenticationResponse, + PublicKeyCredentialCreationOptions, + RegistrationResponse, + UserVerificationRequirement + ) + FIDO2_AVAILABLE = True +except ImportError: + FIDO2_AVAILABLE = False + + +# Error messages +ERROR_MESSAGES = { + 'no_fido2': 'FIDO2 library not available. Please install: pip install fido2', + 'platform_not_supported': 'Biometric authentication not supported on this platform', + 'no_credentials': 'No biometric credentials found. Please register first using "biometric register"', + 'authentication_cancelled': 'Biometric authentication was cancelled', + 'authentication_timeout': 'Biometric authentication timed out', + 'authentication_failed': 'Biometric authentication failed', + 'registration_failed': 'Biometric registration failed', + 'verification_failed': 'Biometric verification failed', + 'credential_exists': 'A biometric credential for this account already exists. Use "biometric unregister" first.', + 'credential_already_registered': 'A biometric credential for this account already exists. Use "biometric unregister" first.', + 'keychain_store_failed': 'Failed to store credential in keychain', + 'touchid_not_available': 'Touch ID is not available or configured', + 'windows_hello_not_setup': 'Windows Hello is available but not yet set up. Please complete the setup in Windows Settings > Accounts > Sign-In options, then try running this command again.', + 'no_matching_credential': 'No matching credential found in keychain' +} + +# Success messages +SUCCESS_MESSAGES = { + 'registration_complete': 'Biometric authentication completed successfully!', + 'unregistration_complete': 'Biometric authentication has been completely removed', + 'verification_success': 'Your biometric authentication is working correctly!', + 'credential_disabled': 'Passkey was successfully disabled and no longer available for login' +} + +# Default credential name template +CREDENTIAL_NAME_TEMPLATE = "Commander CLI ({hostname})" + +# Common authentication reasons +AUTH_REASONS = { + 'register': "Register biometric authentication for {rp_id}", + 'login': "Authenticate with Keeper for {rp_id}", + 'verification': "Verify biometric authentication for {rp_id}" +} \ No newline at end of file diff --git a/keepercli-package/src/keepercli/biometric/utils/error_handler.py b/keepercli-package/src/keepercli/biometric/utils/error_handler.py new file mode 100644 index 00000000..890a6d71 --- /dev/null +++ b/keepercli-package/src/keepercli/biometric/utils/error_handler.py @@ -0,0 +1,130 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Exception: + """Handle authentication errors with consistent messaging""" + error_msg = str(error).lower() + error_str = str(error) + + if any(keyword in error_msg for keyword in ["cancelled", "denied"]): + return Exception(f"{platform_name} authentication cancelled") + elif "timeout" in error_msg: + return Exception(f"{platform_name} authentication timed out") + elif "not available" in error_msg: + return Exception(f"{platform_name} is not available or not set up") + elif any(phrase in error_msg for phrase in ["no identities are enrolled", "biometry is not enrolled", "not enrolled", "biometric not set up", "touch id not set up"]): + return Exception(f"{platform_name} is not set up - please enroll your biometric credentials in system settings") + elif "parameter is incorrect" in error_msg: + return Exception(f"{platform_name} parameter error - please check your biometric setup") + elif any(phrase in error_msg for phrase in ["not configured", "not enabled", "unavailable", "not supported"]): + return Exception(f"{platform_name} is not available or not configured") + elif "no matching credential found" in error_msg: + return Exception(ERROR_MESSAGES['no_matching_credential']) + else: + if "error domain=" in error_msg or "code=" in error_msg: + if "localizeddescription=" in error_msg: + desc_start = error_msg.find("localizeddescription=") + len("localizeddescription=") + desc_end = error_msg.find("}", desc_start) + if desc_end == -1: + desc_end = len(error_msg) + description = error_str[desc_start:desc_end].strip() + return Exception(f"{platform_name} error: {description}") + + return Exception(f"{platform_name} authentication failed: {error_str}") + + @staticmethod + def handle_credential_creation_error(error: Exception, platform_name: str = "Biometric") -> Exception: + """Handle credential creation errors with consistent messaging""" + error_msg = str(error).lower() + error_str = str(error) + + if any(keyword in error_msg for keyword in ["cancelled", "denied"]): + return Exception(f"{platform_name} registration cancelled") + elif ("object already exists" in error_msg or + ("oserror" in error_msg and "22" in error_msg and "object already exists" in error_msg)): + return Exception(f"A biometric credential for this account already exists") + elif "timeout" in error_msg: + return Exception(f"{platform_name} registration timed out") + elif "not available" in error_msg: + return Exception(f"{platform_name} is not available or not set up") + elif any(phrase in error_msg for phrase in ["no identities are enrolled", "biometry is not enrolled", "not enrolled", "biometric not set up", "touch id not set up"]): + return Exception(f"{platform_name} is not set up - please enroll your biometric credentials in system settings") + elif any(phrase in error_msg for phrase in ["not configured", "not enabled", "unavailable", "not supported"]): + return Exception(f"{platform_name} is not available or not configured") + else: + if "error domain=" in error_msg or "code=" in error_msg: + if "localizeddescription=" in error_msg: + desc_start = error_msg.find("localizeddescription=") + len("localizeddescription=") + desc_end = error_msg.find("}", desc_start) + if desc_end == -1: + desc_end = len(error_msg) + description = error_str[desc_start:desc_end].strip() + return Exception(f"{platform_name} error: {description}") + + return Exception(f"{platform_name} registration failed: {error_str}") + + @staticmethod + def handle_command_error(command_name: str, operation: str, error: Exception): + """Handle command errors with clean error messages""" + raise CommandError(str(error)) + + @staticmethod + def handle_keyboard_interrupt(command_name: str, operation: str): + """Handle keyboard interrupt with clean error messages""" + raise CommandError(f'{operation} cancelled by user') + + @staticmethod + def create_platform_error(platform_name: str, error_key: str, additional_info: str = "") -> str: + """Create platform-specific error message""" + base_message = ERROR_MESSAGES.get(error_key, f"Unknown error in {platform_name}") + if additional_info: + return f"{base_message}: {additional_info}" + return base_message + + @staticmethod + def validate_dependencies(required_modules: list, platform_name: str = "Platform") -> None: + """Validate that required modules are available""" + missing_modules = [] + for module in required_modules: + try: + __import__(module) + except ImportError: + missing_modules.append(module) + + if missing_modules: + raise Exception(f"Required {platform_name} dependencies not available: {', '.join(missing_modules)}") + + @staticmethod + def create_storage_error(operation: str, platform_name: str, error: Exception) -> None: + """Log storage operation errors consistently""" + logging.debug(f'Failed to {operation} {platform_name} biometric flag: {error}') + + @staticmethod + def execute_with_error_handling(command_name: str, operation: str, func, *args, **kwargs): + """Execute a function with consistent error handling""" + try: + return func(*args, **kwargs) + except KeyboardInterrupt: + BiometricErrorHandler.handle_keyboard_interrupt(command_name, operation) + except Exception as e: + BiometricErrorHandler.handle_command_error(command_name, operation, e) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/breachwatch.py b/keepercli-package/src/keepercli/commands/breachwatch.py index 3900b094..d9a34803 100644 --- a/keepercli-package/src/keepercli/commands/breachwatch.py +++ b/keepercli-package/src/keepercli/commands/breachwatch.py @@ -472,4 +472,4 @@ def _extract_euids(self, scan_results: list) -> list: password, scan_result = result if hasattr(scan_result, 'euid') and scan_result.euid: euids.append(scan_result.euid) - return euids \ No newline at end of file + return euids diff --git a/keepercli-package/src/keepercli/login.py b/keepercli-package/src/keepercli/login.py index 29e55efb..69fb098e 100644 --- a/keepercli-package/src/keepercli/login.py +++ b/keepercli-package/src/keepercli/login.py @@ -18,6 +18,8 @@ from . import prompt_utils, constants from .params import KeeperParams +DEVICE_APPROVAL_ERRORS = ["device_needs_approval", "device approval"] + class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): def output_text(self, text: str) -> None: @@ -78,9 +80,16 @@ def keeper_redirect(region): auth.alternate_password = sso_master_password auth.login(username, *passwords) + + from .biometric import check_biometric_previously_used + biometric_present = check_biometric_previously_used(username) + while not auth.login_step.is_final(): step = auth.login_step - if isinstance(step, login_auth.LoginStepDeviceApproval): + + if biometric_present and not isinstance(step, login_auth._ConnectedLoginStep): + biometric_present = LoginFlow.handle_biometric_password_step(auth, username, keeper_endpoint.client_version) + elif isinstance(step, login_auth.LoginStepDeviceApproval): LoginFlow.verify_device(step) elif isinstance(step, login_auth.LoginStepTwoFactor): LoginFlow.handle_two_factor(context, step) @@ -354,6 +363,65 @@ def handle_two_factor(context: KeeperParams, step: login_auth.LoginStepTwoFactor except errors.KeeperApiError as kae: prompt_utils.output_text(f'Invalid 2FA code: ({kae.result_code}) {kae.message}') + @staticmethod + def handle_biometric_password_step(login_auth_context: login_auth.LoginAuth, username: str, client_version: str) -> bool: + """Handle biometric authentication as part of the password verification step""" + logger = utils.get_logger() + + from .biometric.commands.verify import BiometricVerifyCommand + + logger.info("Attempting biometric authentication...") + logger.info("Press Ctrl+C to skip biometric and use password") + + try: + auth_helper = BiometricVerifyCommand() + biometric_result = auth_helper.biometric_authenticate(login_auth_context, client_version, username, purpose='login') + + if biometric_result and biometric_result.isValid: + logger.info("Biometric authentication successful!") + login_auth_context.context.biometric = True + login_auth._resume_login(login_auth_context, biometric_result.encryptedLoginToken, method=APIRequest_pb2.EXISTING_ACCOUNT) + return True + else: + return LoginFlow._handle_biometric_failure(logger, "Biometric authentication failed") + + except KeyboardInterrupt: + return LoginFlow._handle_biometric_cancellation(logger) + + except Exception as e: + return LoginFlow._handle_biometric_error(logger, e) + + @staticmethod + def _handle_biometric_failure(logger, message: str) -> bool: + """Handle biometric authentication failure""" + logger.info(message) + prompt_utils.output_text("Biometric authentication failed. Please use password authentication.") + return False + + @staticmethod + def _handle_biometric_cancellation(logger) -> bool: + """Handle biometric authentication cancellation by user""" + logger.info("Biometric authentication cancelled by user") + prompt_utils.output_text("Biometric authentication cancelled. Using password authentication.") + return False + + @staticmethod + def _handle_biometric_error(logger, error: Exception) -> bool: + """Handle biometric authentication errors""" + error_message = str(error).lower() + + if any(pattern in error_message for pattern in DEVICE_APPROVAL_ERRORS): + logger.error("\nBiometric Login Failed") + logger.warning("Device registration required for biometric authentication.") + logger.warning("\nPlease run: this-device register") + logger.warning("Then try biometric login again.") + prompt_utils.output_text("Device needs approval for biometric authentication. Using password authentication.") + else: + logger.info(f"Biometric authentication error: {error}") + prompt_utils.output_text("Biometric authentication error. Using password authentication.") + + return False + @staticmethod def verify_device(step: login_auth.LoginStepDeviceApproval): menu = [ diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 62d6854d..1381324e 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -13,10 +13,12 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Account): from .commands import account_commands + from .biometric import BiometricCommand commands.register_command('server', base.GetterSetterCommand('server', 'Sets or displays current Keeper region'), base.CommandScope.Account) commands.register_command('login', account_commands.LoginCommand(), base.CommandScope.Account) + commands.register_command('biometric', BiometricCommand(), base.CommandScope.Account) commands.register_command('logout', account_commands.LogoutCommand(), base.CommandScope.Account) commands.register_command('this-device', account_commands.ThisDeviceCommand(), base.CommandScope.Account) commands.register_command('whoami', account_commands.WhoamiCommand(), base.CommandScope.Account) diff --git a/keepersdk-package/src/keepersdk/authentication/login_auth.py b/keepersdk-package/src/keepersdk/authentication/login_auth.py index 19e86dac..f33905f0 100644 --- a/keepersdk-package/src/keepersdk/authentication/login_auth.py +++ b/keepersdk-package/src/keepersdk/authentication/login_auth.py @@ -193,6 +193,7 @@ def __init__(self) -> None: self.message_session_uid: bytes = crypto.get_random_bytes(16) self.account_type: AccountAuthType = AccountAuthType.Regular self.sso_login_info: Optional[keeper_auth.SsoLoginInfo] = None + self.biometric: Optional[bool] = False class LoginAuth: @@ -533,7 +534,9 @@ def _process_start_login(login: LoginAuth, request: APIRequest_pb2.StartLoginReq def decrypt_with_device_key(encrypted_data_key): return crypto.decrypt_ec(encrypted_data_key, login.context.device_private_key) _on_logged_in(login, response, decrypt_with_device_key) - if login.context.sso_login_info is None: + if login.context.biometric: + utils.get_logger().info('Successfully authenticated with Biometric Login') + elif login.context.sso_login_info is None: utils.get_logger().info('Successfully authenticated with Persistent Login') else: utils.get_logger().info('Successfully authenticated with %s SSO', From 628120d83674ba57452dc5ca02839f66b3bece59 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 12 Sep 2025 17:58:37 +0530 Subject: [PATCH 27/44] Password-report command and bug fixes --- .../keepercli/commands/enterprise_utils.py | 2 + .../src/keepercli/commands/password_report.py | 316 ++++++++++++++++++ .../src/keepercli/commands/record_edit.py | 64 ++-- .../src/keepercli/commands/record_type.py | 4 +- .../src/keepercli/commands/vault_folder.py | 1 - keepercli-package/src/keepercli/login.py | 7 +- .../src/keepercli/register_commands.py | 3 +- .../src/keepersdk/vault/attachment.py | 4 +- .../src/keepersdk/vault/record_management.py | 4 + 9 files changed, 373 insertions(+), 32 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/password_report.py diff --git a/keepercli-package/src/keepercli/commands/enterprise_utils.py b/keepercli-package/src/keepercli/commands/enterprise_utils.py index a17562ce..1a1f646e 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_utils.py +++ b/keepercli-package/src/keepercli/commands/enterprise_utils.py @@ -158,6 +158,8 @@ def resolve_existing_roles(e_data: enterprise_types.IEnterpriseData, role_names: r = rr[0] elif len(rr) >= 2: raise base.CommandError(f'Role name "{role_name}" is not unique. Use Role ID.') + elif isinstance(rr, enterprise_types.Role): + r = rr if r is None: raise base.CommandError(f'Role name "{role_name}" is not found') found_roles[r.role_id] = r diff --git a/keepercli-package/src/keepercli/commands/password_report.py b/keepercli-package/src/keepercli/commands/password_report.py new file mode 100644 index 00000000..5fb59c8b --- /dev/null +++ b/keepercli-package/src/keepercli/commands/password_report.py @@ -0,0 +1,316 @@ + +import argparse +from collections import namedtuple +from typing import Optional, Dict, Tuple, Any + +from . import base +from .. import api +from ..helpers import folder_utils, report_utils +from ..params import KeeperParams + +from keepersdk import utils +from keepersdk.vault import vault_record, vault_extensions + + +logger = api.get_logger() + +PW_SPECIAL_CHARACTERS = '!@#$%()+;<>=?[]{}^.,' +DEFAULT_TRUNCATION_LENGTH = 32 +SUPPORTED_RECORD_VERSIONS = (2, 3) + +PasswordStrength = namedtuple('PasswordStrength', 'length caps lower digits symbols') + + +class PasswordReportCommand(base.ArgparseCommand): + """Command to generate password compliance reports for vault records.""" + + def __init__(self) -> None: + """Initialize the password report command.""" + self.parser = argparse.ArgumentParser( + prog='password-report', parents=[base.report_output_parser], description='Display record password report.' + ) + PasswordReportCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Display verbose information') + parser.add_argument('--policy', dest='policy', action='store', + help='Password complexity policy. Length,Lower,Upper,Digits,Special. Default is 12,2,2,2,0') + parser.add_argument('-l', '--length', dest='length', type=int, action='store', help='Minimum password length.') + parser.add_argument('-u', '--upper', dest='upper', type=int, action='store', help='Minimum uppercase characters.') + parser.add_argument('--lower', dest='lower', type=int, action='store', help='Minimum lowercase characters.') + parser.add_argument('-d', '--digits', dest='digits', type=int, action='store', help='Minimum digits.') + parser.add_argument('-s', '--special', dest='special', type=int, action='store', help='Minimum special characters.') + parser.add_argument('folder', nargs='?', type=str, action='store', help='folder path or UID') + + def _parse_password_policy(self, kwargs: Dict[str, Any]) -> Tuple[int, int, int, int, int]: + """Parse password policy from command line arguments. + + Returns: + tuple: (length, lower, upper, digits, special) requirements + """ + p_length = p_lower = p_upper = p_digits = p_special = 0 + + policy = kwargs.get('policy') + if policy: + comps = [x.strip() for x in policy.split(',')] + if any(False for c in comps if len(c) > 0 and not c.isdigit()): + raise base.CommandError('Invalid policy format. Must be list of integer values separated by commas.') + + # Parse policy components with bounds checking + policy_values = [int(comp) if comp else 0 for comp in comps[:5]] + p_length, p_lower, p_upper, p_digits, p_special = policy_values + [0] * (5 - len(policy_values)) + else: + # Parse individual arguments + p_length = kwargs.get('length', 0) if isinstance(kwargs.get('length'), int) else 0 + p_upper = kwargs.get('upper', 0) if isinstance(kwargs.get('upper'), int) else 0 + p_lower = kwargs.get('lower', 0) if isinstance(kwargs.get('lower'), int) else 0 + p_digits = kwargs.get('digits', 0) if isinstance(kwargs.get('digits'), int) else 0 + p_special = kwargs.get('special', 0) if isinstance(kwargs.get('special'), int) else 0 + + if p_length <= 0 and p_upper <= 0 and p_lower <= 0 and p_digits <= 0 and p_special <= 0: + self.get_parser().print_help() + raise base.CommandError('Password policy must be specified.') + + return p_length, p_lower, p_upper, p_digits, p_special + + def _resolve_folder_uid(self, context: KeeperParams, path_or_uid: Optional[str]) -> str: + """Resolve folder path or UID to folder UID. + + Args: + context: Keeper parameters + path_or_uid: Folder path or UID + + Returns: + str: Folder UID + + Raises: + CommandError: If folder not found + """ + if not path_or_uid: + return '' + + # Get by UID + if path_or_uid in context.vault.vault_data._folders: + return path_or_uid + + # Try to resolve as path + rs = folder_utils.try_resolve_path(context, path_or_uid) + if rs is None: + raise base.CommandError(f'Folder path {path_or_uid} not found') + + folder, pattern = rs + if not folder or pattern: + raise base.CommandError(f'Folder path {path_or_uid} not found') + + return folder.uid or '' + + def _extract_password_from_record(self, record: Any) -> str: + """Extract password from a vault record. + + Args: + record: Vault record object + + Returns: + str: Password string or empty string if not found + """ + if isinstance(record, vault_record.PasswordRecord): + return record.password + elif isinstance(record, vault_record.TypedRecord): + password_field = record.get_typed_field('password') + if password_field: + return password_field.get_default_value(str) + return '' + + def _check_password_policy_compliance(self, strength: 'PasswordStrength', p_length: int, p_lower: int, p_upper: int, p_digits: int, p_special: int) -> bool: + """Check if password meets policy requirements. + + Args: + strength: PasswordStrength object + p_length: Minimum length requirement + p_lower: Minimum lowercase requirement + p_upper: Minimum uppercase requirement + p_digits: Minimum digits requirement + p_special: Minimum special characters requirement + + Returns: + bool: True if password meets all requirements + """ + return (strength.length >= p_length and + strength.caps >= p_upper and + strength.lower >= p_lower and + strength.digits >= p_digits and + strength.symbols >= p_special) + + def _truncate_text(self, text: str, max_length: int = DEFAULT_TRUNCATION_LENGTH) -> str: + """Truncate text to specified length with ellipsis. + + Args: + text: Text to truncate + max_length: Maximum length + + Returns: + str: Truncated text + """ + if len(text) > max_length: + return text[:max_length-2] + '...' + return text + + def _build_password_count_map(self, context: KeeperParams) -> Dict[str, int]: + """Build a map of password usage counts from breach watch records. + + Args: + context: Keeper parameters + + Returns: + dict: Password to count mapping + """ + password_count = {} + for record_uid, bw_record in context.vault.vault_data._breach_watch_records: + if record_uid in context.vault.vault_data._records: + if isinstance(bw_record, dict): + data = bw_record.get('data_unencrypted') + if isinstance(data, dict): + passwords = data.get('passwords') + if isinstance(passwords, list): + for pwd in passwords: + password = pwd.get('value') + if password: + password_count[password] = password_count.get(password, 0) + 1 + return password_count + + def _get_breach_watch_status(self, context: KeeperParams, record_uid: str, password: str) -> Tuple[str, Optional[int]]: + """Get breach watch status and reuse count for a password. + + Args: + context: Keeper parameters + record_uid: Record UID + password: Password to check + + Returns: + tuple: (status, reuse_count) + """ + status = '' + reused = None + + bw_record = context.vault.vault_data.get_breach_watch_record(record_uid) + if isinstance(bw_record, dict): + data = bw_record.get('data_unencrypted') + if isinstance(data, dict): + passwords = data.get('passwords') + if isinstance(passwords, list): + password_status = next((x for x in passwords if x.get('value') == password), None) + if isinstance(password_status, dict): + status = password_status.get('status', '') + + return status, reused + + def _display_policy_summary(self, p_length: int, p_lower: int, p_upper: int, p_digits: int, p_special: int): + """Display password policy requirements summary. + + Args: + p_length: Minimum length requirement + p_lower: Minimum lowercase requirement + p_upper: Minimum uppercase requirement + p_digits: Minimum digits requirement + p_special: Minimum special characters requirement + """ + logger.info('') + if p_length > 0: + logger.info(' Password Length: %d', p_length) + if p_lower > 0: + logger.info('Lowercase characters: %d', p_lower) + if p_upper > 0: + logger.info('Uppercase characters: %d', p_upper) + if p_digits > 0: + logger.info(' Digits: %d', p_digits) + if p_special > 0: + logger.info(' Special characters: %d', p_special) + logger.info('') + + def execute(self, context: KeeperParams, **kwargs: Any) -> Any: + verbose = kwargs.get('verbose') is True + p_length, p_lower, p_upper, p_digits, p_special = self._parse_password_policy(kwargs) + + path_or_uid = kwargs.get('folder') + folder_uid = self._resolve_folder_uid(context, path_or_uid) + + folder = context.vault.vault_data.get_folder(folder_uid) + records = folder.records + report_table = [] + report_header = ['record_uid', 'title', 'description', 'length', 'lower', 'upper', 'digits', 'special'] + breach_watch_plugin = context.vault.breach_watch_plugin() + + if verbose: + report_header.append('score') + if breach_watch_plugin: + report_header.extend(['status', 'reused']) + password_usage_count = self._build_password_count_map(context) + else: + password_usage_count = {} + + output_format = kwargs.get('format') + for record_uid in records: + record = context.vault.vault_data.load_record(record_uid) + if not record or record.version not in SUPPORTED_RECORD_VERSIONS: + continue + + password = self._extract_password_from_record(record) + if not password: + continue + + strength = get_password_strength(password) + if self._check_password_policy_compliance(strength, p_length, p_lower, p_upper, p_digits, p_special): + continue + + title = self._truncate_text(record.title) + description = vault_extensions.get_record_description(record) + if isinstance(description, str): + description = self._truncate_text(description) + report_row = [record_uid, title, description, strength.length, strength.lower, strength.caps, strength.digits, strength.symbols] + if verbose: + report_row.append(utils.password_score(password)) + if breach_watch_plugin: + status, _ = self._get_breach_watch_status(context, record_uid, password) + reused_count = None + if password in password_usage_count: + count = password_usage_count[password] + if isinstance(count, int) and count > 1: + reused_count = count + report_row.extend([status, reused_count]) + + report_table.append(report_row) + + if output_format != 'json': + report_header = [report_utils.field_to_title(x) for x in report_header] + + self._display_policy_summary(p_length, p_lower, p_upper, p_digits, p_special) + + return report_utils.dump_report_data(report_table, report_header, fmt=output_format, filename=kwargs.get('output'), row_number=True) + + +def get_password_strength(password: str) -> 'PasswordStrength': + """Analyze password strength and return character counts. + + Args: + password: Password string to analyze + + Returns: + PasswordStrength: Named tuple with character counts + """ + length = len(password) + caps = lower = digits = symbols = 0 + + for char in password: + if char.isalpha(): + if char.isupper(): + caps += 1 + else: + lower += 1 + elif char.isdigit(): + digits += 1 + elif char in PW_SPECIAL_CHARACTERS: + symbols += 1 + + return PasswordStrength(length=length, caps=caps, lower=lower, digits=digits, symbols=symbols) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index 6d9882b8..e616c323 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -15,7 +15,7 @@ record_management, vault_online, vault_data, vault_types, vault_utils, vault_extensions) from keepersdk import crypto, generator -from . import base +from . import base, enterprise_utils from .. import prompt_utils, api, constants from ..helpers import folder_utils, record_utils, report_utils, share_utils, timeout_utils from ..params import KeeperParams @@ -1113,7 +1113,7 @@ def execute(self, context: KeeperParams, **kwargs): else: raise base.CommandError('The given UID or title is not a valid folder') elif team: - team = self._find_team(context.vault, team) + team = self._find_team(context, team) if team: target_object = ('team', team) else: @@ -1125,23 +1125,24 @@ def execute(self, context: KeeperParams, **kwargs): else: raise base.CommandError('The given UID or title is not a valid record') elif uid: - target_object = self._find_target_object(context.vault, uid) + target_object = self._find_target_object(context, uid) else: raise base.CommandError('Either UID parameter or one of -f, -t, -r flags is required') if not target_object: raise base.CommandError('The given UID is not a valid Keeper Object') - self._display_object(context.vault, target_object, output_format, unmask) + self._display_object(context, target_object, output_format, unmask) def _validate_context(self, context: KeeperParams): """Validate that the vault is properly initialized.""" if not context.vault: raise ValueError("Vault is not initialized.") - def _find_target_object(self, vault: vault_data.VaultData, uid_or_title: str): + def _find_target_object(self, context: KeeperParams, uid_or_title: str): """Find a Keeper object (record, folder, shared folder, or team) by UID or title.""" + vault = context.vault shared_folder = self._find_shared_folder(vault, uid_or_title) if shared_folder: return ('shared_folder', shared_folder) @@ -1150,7 +1151,7 @@ def _find_target_object(self, vault: vault_data.VaultData, uid_or_title: str): if folder: return ('folder', folder) - team = self._find_team(vault, uid_or_title) + team = self._find_team(context, uid_or_title) if team: return ('team', team) @@ -1184,18 +1185,18 @@ def _find_folder(self, vault: vault_data.VaultData, uid_or_title: str): None ) - def _find_team(self, vault: vault_data.VaultData, uid_or_title: str): + def _find_team(self, context: KeeperParams, uid_or_title: str): """Find a team by UID or name.""" - return next( - (t for t in vault.vault_data.teams() - if t.team_uid == uid_or_title or t.name == uid_or_title), - None - ) + if not context.enterprise_data: + raise base.CommandError('You must be an enterprise admin to use this command') + + team = enterprise_utils.TeamUtils.resolve_single_team(context.enterprise_data, uid_or_title) + return team - def _display_object(self, vault: vault_data.VaultData, target_object, output_format: str, unmask: bool): + def _display_object(self, context: KeeperParams, target_object, output_format: str, unmask: bool): """Display the target object in the specified format.""" object_type, object_data = target_object - + vault = context.vault if object_type == 'record': self._display_record(vault, object_data, output_format, unmask) elif object_type == 'shared_folder': @@ -1203,7 +1204,7 @@ def _display_object(self, vault: vault_data.VaultData, target_object, output_for elif object_type == 'folder': self._display_folder(vault, object_data, output_format) elif object_type == 'team': - self._display_team(vault, object_data, output_format) + self._display_team(context, object_data, output_format) def _display_record(self, vault: vault_data.VaultData, record, output_format: str, unmask: bool): """Display a record in the specified format.""" @@ -1231,12 +1232,12 @@ def _display_folder(self, vault: vault_data.VaultData, folder, output_format: st else: # detail format self._display_folder_detail(vault, folder.folder_uid) - def _display_team(self, vault: vault_data.VaultData, team, output_format: str): + def _display_team(self, context: KeeperParams, team, output_format: str): """Display a team in the specified format.""" if output_format == 'json': - self._display_team_json(vault, team.team_uid) + self._display_team_json(context, team.team_uid) else: # detail format - self._display_team_detail(vault, team.team_uid) + self._display_team_detail(context, team.team_uid) def _display_record_json(self, vault: vault_data.VaultData, uid: str, unmask: bool = False): """Display record information in JSON format.""" @@ -1388,9 +1389,13 @@ def _display_folder_json(self, vault: vault_data.VaultData, uid: str): } logger.info(json.dumps(output, indent=2)) - def _display_team_json(self, vault: vault_data.VaultData, uid: str): + def _display_team_json(self, context: KeeperParams, uid: str): """Display team information in JSON format.""" - team = vault.vault_data.get_team(team_uid=uid) + team = context.enterprise_data.teams.get_entity(uid) + user = enterprise_utils.UserUtils.resolve_single_user(context.enterprise_data, context.username) + team_users = {x.team_uid for x in context.enterprise_data.team_users.get_links_by_object(user.enterprise_user_id)} + if team.team_uid not in team_users: + logger.info(f'User {context.username} does not belong to team {team.name}') output = { 'Team UID:': uid, 'Name:': team.name @@ -1617,15 +1622,24 @@ def _display_folder_detail(self, vault: vault_data.VaultData, uid: str): if folder.folder_type == 'shared_folder_folder': logger.info('{0:>20s}: {1:<20s}'.format('Shared Folder UID', folder.folder_scope_uid)) - def _display_team_detail(self, vault: vault_data.VaultData, uid: str): + def _display_team_detail(self, context: KeeperParams, uid: str): """Display team information in detailed format.""" - team = vault.vault_data.get_team(team_uid=uid) + team = context.enterprise_data.teams.get_entity(uid) + + user = enterprise_utils.UserUtils.resolve_single_user(context.enterprise_data, context.username) + team_users = {x.team_uid for x in context.enterprise_data.team_users.get_links_by_object(user.enterprise_user_id)} + team_user = True + if team.team_uid not in team_users: + logger.info(f'User {context.username} does not belong to team {team.name}') + team_user = False + logger.info('') logger.info('{0:>20s}: {1:<20s}'.format('Team UID', team.team_uid)) logger.info('{0:>20s}: {1}'.format('Name', team.name)) - logger.info('{0:>20s}: {1}'.format('Restrict Edit', team.restrict_edit)) - logger.info('{0:>20s}: {1}'.format('Restrict View', team.restrict_view)) - logger.info('{0:>20s}: {1}'.format('Restrict Share', team.restrict_share)) + if team_user: + logger.info('{0:>20s}: {1}'.format('Restrict Edit', team.restrict_edit)) + logger.info('{0:>20s}: {1}'.format('Restrict View', team.restrict_view)) + logger.info('{0:>20s}: {1}'.format('Restrict Share', team.restrict_share)) logger.info('') def _display_record_password(self, vault: vault_data.VaultData, uid: str): diff --git a/keepercli-package/src/keepercli/commands/record_type.py b/keepercli-package/src/keepercli/commands/record_type.py index 37837f49..47243fa9 100644 --- a/keepercli-package/src/keepercli/commands/record_type.py +++ b/keepercli-package/src/keepercli/commands/record_type.py @@ -208,10 +208,10 @@ def execute(self, context: KeeperParams, **kwargs) -> None: record_type.id, record_type.name, scope, - fields[0].label if hasattr(fields[0], 'label') else str(fields[0]) + fields[0].label if hasattr(fields[0], 'label') and fields[0].label != '' else str(fields[0].type) ]) for field in fields[1:]: - rows.append(['', '', '', field.label if hasattr(field, 'label') else str(field)]) + rows.append(['', '', '', field.label if hasattr(field, 'label') and field.label != '' else str(field.type)]) headers = ('id', 'name', 'scope', 'fields') return report_utils.dump_report_data(rows, headers, column_width='auto', fmt='simple') diff --git a/keepercli-package/src/keepercli/commands/vault_folder.py b/keepercli-package/src/keepercli/commands/vault_folder.py index f7fcd085..9b8f730d 100644 --- a/keepercli-package/src/keepercli/commands/vault_folder.py +++ b/keepercli-package/src/keepercli/commands/vault_folder.py @@ -465,7 +465,6 @@ class FolderMoveCommand(base.ArgparseCommand, _FolderMixin): help='apply \"Can Share\" record permission') parser.add_argument('-e', '--can-edit', dest='can_edit', action='store', choices=['on', 'off'], help='apply \"Can Edit\" record permission') - group = parser.add_mutually_exclusive_group() parser.add_argument('src', nargs='+', type=str, metavar='PATH', help='source path to folder/record, search pattern or record UID') parser.add_argument('dst', type=str, diff --git a/keepercli-package/src/keepercli/login.py b/keepercli-package/src/keepercli/login.py index 69fb098e..1cd1ddd9 100644 --- a/keepercli-package/src/keepercli/login.py +++ b/keepercli-package/src/keepercli/login.py @@ -89,6 +89,10 @@ def keeper_redirect(region): if biometric_present and not isinstance(step, login_auth._ConnectedLoginStep): biometric_present = LoginFlow.handle_biometric_password_step(auth, username, keeper_endpoint.client_version) + if not isinstance(auth.login_step, login_auth._ConnectedLoginStep): + logger.error('Biometric authentication failed, falling back to default login method. Device might not be registered') + else: + logger.info("Biometric authentication successful!") elif isinstance(step, login_auth.LoginStepDeviceApproval): LoginFlow.verify_device(step) elif isinstance(step, login_auth.LoginStepTwoFactor): @@ -378,10 +382,9 @@ def handle_biometric_password_step(login_auth_context: login_auth.LoginAuth, use biometric_result = auth_helper.biometric_authenticate(login_auth_context, client_version, username, purpose='login') if biometric_result and biometric_result.isValid: - logger.info("Biometric authentication successful!") login_auth_context.context.biometric = True login_auth._resume_login(login_auth_context, biometric_result.encryptedLoginToken, method=APIRequest_pb2.EXISTING_ACCOUNT) - return True + return False else: return LoginFlow._handle_biometric_failure(logger, "Biometric authentication failed") diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 1381324e..8b897275 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -26,7 +26,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, - record_type, secrets_manager, share_management) + record_type, secrets_manager, share_management, password_report) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -49,6 +49,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('import', importer_commands.ImportCommand(), base.CommandScope.Vault) commands.register_command('export', importer_commands.ExportCommand(), base.CommandScope.Vault) commands.register_command('breachwatch', breachwatch.BreachWatchCommand(), base.CommandScope.Vault, 'bw') + commands.register_command('password-report', password_report.PasswordReportCommand(), base.CommandScope.Vault) commands.register_command('record-type-add', record_type.RecordTypeAddCommand(), base.CommandScope.Vault) commands.register_command('record-type-edit', record_type.RecordTypeEditCommand(), base.CommandScope.Vault) commands.register_command('record-type-delete', record_type.RecordTypeDeleteCommand(), base.CommandScope.Vault) diff --git a/keepersdk-package/src/keepersdk/vault/attachment.py b/keepersdk-package/src/keepersdk/vault/attachment.py index a52853ef..0d56fa5d 100644 --- a/keepersdk-package/src/keepersdk/vault/attachment.py +++ b/keepersdk-package/src/keepersdk/vault/attachment.py @@ -13,6 +13,7 @@ from .vault_extensions import resolve_record_access_path from .vault_record import FileRecord, PasswordRecord, TypedRecord, AttachmentFile, AttachmentFileThumb from .. import utils, crypto +from ..authentication import endpoint from ..proto import record_pb2 @@ -165,6 +166,7 @@ class FileUploadTask(UploadTask): def __init__(self, file_path: str) -> None: super().__init__() self.file_path = file_path + self.name = os.path.basename(self.file_path) def prepare(self): self.file_path = os.path.expanduser(self.file_path) @@ -178,7 +180,7 @@ def prepare(self): @contextlib.contextmanager def open(self): - yield open(self.file_path, 'r') + yield open(self.file_path, 'rb') def upload_attachments(vault: vault_online.VaultOnline, diff --git a/keepersdk-package/src/keepersdk/vault/record_management.py b/keepersdk-package/src/keepersdk/vault/record_management.py index 517ea450..a362ac76 100644 --- a/keepersdk-package/src/keepersdk/vault/record_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_management.py @@ -447,11 +447,15 @@ def notify_on_warning(message: str) -> None: for f in folders: if f.folder_type == 'user_folder': scope_uid = '' + selected_folder_uid = f.folder_uid + break else: scope_uid = f.folder_scope_uid or '' if scope_uid == dst_scope_uid: selected_folder_uid = f.folder_uid break + else: + selected_folder_uid = f.folder_scope_uid if selected_folder_uid is None: selected_folder_uid = next((x for x in record_uid_to_move)) folders = [x for x in folders if x.folder_uid != selected_folder_uid] From e211e98a2f3a47784cb63f24740a7a495f9e8b43 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 12 Sep 2025 18:15:03 +0530 Subject: [PATCH 28/44] Added examples for enterprise and record attachment commands --- examples/audit_alert/audit_alert_add.py | 6 +- examples/audit_alert/audit_alert_delete.py | 4 + examples/audit_alert/audit_alert_edit.py | 4 + examples/audit_alert/audit_alert_history.py | 115 ++++++++++++++ examples/audit_alert/audit_alert_list.py | 4 + examples/audit_alert/audit_alert_view.py | 4 + examples/audit_report/audit_report.py | 141 ++++++++++++++++++ examples/breachwatch/breachwatch_ignore.py | 4 + examples/breachwatch/breachwatch_list.py | 119 +++++++++++++++ examples/breachwatch/breachwatch_scan.py | 4 + .../create_custom_record_type.py | 10 +- .../custom_record_type_info.py | 16 +- .../delete_custom_record_type.py | 18 +-- .../download_record_types.py | 9 +- .../edit_custom_record_type.py | 12 +- .../custom_record_type/load_record_types.py | 10 +- .../enterprise_info/enterprise_info_node.py | 117 +++++++++++++++ .../enterprise_info/enterprise_info_role.py | 117 +++++++++++++++ .../enterprise_info/enterprise_info_team.py | 117 +++++++++++++++ .../enterprise_info/enterprise_info_tree.py | 117 +++++++++++++++ .../enterprise_info/enterprise_info_user.py | 117 +++++++++++++++ .../enterprise_node/enterprise_node_add.py | 117 +++++++++++++++ .../enterprise_node/enterprise_node_delete.py | 114 ++++++++++++++ .../enterprise_node/enterprise_node_edit.py | 118 +++++++++++++++ .../enterprise_node_invite_email.py | 118 +++++++++++++++ .../enterprise_node_set_logo.py | 117 +++++++++++++++ .../enterprise_node/enterprise_node_view.py | 115 ++++++++++++++ .../enterprise_node_wipe_out.py | 115 ++++++++++++++ .../enterprise_role/enterprise_role_add.py | 119 +++++++++++++++ .../enterprise_role/enterprise_role_admin.py | 120 +++++++++++++++ .../enterprise_role/enterprise_role_copy.py | 118 +++++++++++++++ .../enterprise_role/enterprise_role_delete.py | 114 ++++++++++++++ .../enterprise_role/enterprise_role_edit.py | 122 +++++++++++++++ .../enterprise_role_membership.py | 116 ++++++++++++++ .../enterprise_role/enterprise_role_view.py | 115 ++++++++++++++ .../enterprise_team/enterprise_team_add.py | 117 +++++++++++++++ .../enterprise_team/enterprise_team_delete.py | 114 ++++++++++++++ .../enterprise_team/enterprise_team_edit.py | 118 +++++++++++++++ .../enterprise_team_membership.py | 121 +++++++++++++++ .../enterprise_team/enterprise_team_view.py | 115 ++++++++++++++ .../enterprise_user/enterprise_user_action.py | 116 ++++++++++++++ .../enterprise_user/enterprise_user_add.py | 117 +++++++++++++++ .../enterprise_user/enterprise_user_alias.py | 124 +++++++++++++++ .../enterprise_user/enterprise_user_delete.py | 115 ++++++++++++++ .../enterprise_user/enterprise_user_edit.py | 117 +++++++++++++++ .../enterprise_user/enterprise_user_view.py | 115 ++++++++++++++ examples/folder/share_folder.py | 12 +- .../importing_exporting/apply_membership.py | 116 ++++++++++++++ .../download_membership.py | 122 +++++++++++++++ examples/importing_exporting/export_data.py | 130 ++++++++++++++++ examples/importing_exporting/import_data.py | 126 ++++++++++++++++ .../one_time_share/create_one_time_share.py | 8 +- .../one_time_share/list_one_time_shares.py | 10 +- .../one_time_share/remove_one_time_share.py | 6 +- examples/record/add_record.py | 12 +- examples/record/delete_attachment.py | 118 +++++++++++++++ examples/record/delete_record.py | 12 +- examples/record/download_attachment.py | 121 +++++++++++++++ examples/record/get_command.py | 10 +- examples/record/list_records.py | 7 +- examples/record/share_record.py | 9 +- examples/record/update_record.py | 12 +- examples/record/upload_attachment.py | 118 +++++++++++++++ .../create_secrets_manager_app.py | 12 +- .../get_secrets_manager_app.py | 10 +- .../list_secrets_manager_apps.py | 10 +- .../remove_secrets_manager_app.py | 15 +- .../secrets_manager_app_add_record.py | 8 +- .../secrets_manager_app_remove_record.py | 6 +- .../secrets_manager_client_add.py | 12 +- .../secrets_manager_client_remove.py | 8 +- .../share_secrets_manager_app.py | 8 +- .../unshare_secrets_manager_app.py | 6 +- 73 files changed, 4945 insertions(+), 91 deletions(-) create mode 100644 examples/audit_alert/audit_alert_history.py create mode 100644 examples/audit_report/audit_report.py create mode 100644 examples/breachwatch/breachwatch_list.py create mode 100644 examples/enterprise_info/enterprise_info_node.py create mode 100644 examples/enterprise_info/enterprise_info_role.py create mode 100644 examples/enterprise_info/enterprise_info_team.py create mode 100644 examples/enterprise_info/enterprise_info_tree.py create mode 100644 examples/enterprise_info/enterprise_info_user.py create mode 100644 examples/enterprise_node/enterprise_node_add.py create mode 100644 examples/enterprise_node/enterprise_node_delete.py create mode 100644 examples/enterprise_node/enterprise_node_edit.py create mode 100644 examples/enterprise_node/enterprise_node_invite_email.py create mode 100644 examples/enterprise_node/enterprise_node_set_logo.py create mode 100644 examples/enterprise_node/enterprise_node_view.py create mode 100644 examples/enterprise_node/enterprise_node_wipe_out.py create mode 100644 examples/enterprise_role/enterprise_role_add.py create mode 100644 examples/enterprise_role/enterprise_role_admin.py create mode 100644 examples/enterprise_role/enterprise_role_copy.py create mode 100644 examples/enterprise_role/enterprise_role_delete.py create mode 100644 examples/enterprise_role/enterprise_role_edit.py create mode 100644 examples/enterprise_role/enterprise_role_membership.py create mode 100644 examples/enterprise_role/enterprise_role_view.py create mode 100644 examples/enterprise_team/enterprise_team_add.py create mode 100644 examples/enterprise_team/enterprise_team_delete.py create mode 100644 examples/enterprise_team/enterprise_team_edit.py create mode 100644 examples/enterprise_team/enterprise_team_membership.py create mode 100644 examples/enterprise_team/enterprise_team_view.py create mode 100644 examples/enterprise_user/enterprise_user_action.py create mode 100644 examples/enterprise_user/enterprise_user_add.py create mode 100644 examples/enterprise_user/enterprise_user_alias.py create mode 100644 examples/enterprise_user/enterprise_user_delete.py create mode 100644 examples/enterprise_user/enterprise_user_edit.py create mode 100644 examples/enterprise_user/enterprise_user_view.py create mode 100644 examples/importing_exporting/apply_membership.py create mode 100644 examples/importing_exporting/download_membership.py create mode 100644 examples/importing_exporting/export_data.py create mode 100644 examples/importing_exporting/import_data.py create mode 100644 examples/record/delete_attachment.py create mode 100644 examples/record/download_attachment.py create mode 100644 examples/record/upload_attachment.py diff --git a/examples/audit_alert/audit_alert_add.py b/examples/audit_alert/audit_alert_add.py index fb74a089..c68ffc14 100644 --- a/examples/audit_alert/audit_alert_add.py +++ b/examples/audit_alert/audit_alert_add.py @@ -92,6 +92,7 @@ def execute_audit_alert_add(context: KeeperParams, **kwargs): frequency = "event" audit_events = ["login"] + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -102,7 +103,7 @@ def execute_audit_alert_add(context: KeeperParams, **kwargs): 'name': alert_name, 'frequency': frequency, 'audit_event': audit_events, - 'active': 'on' + 'active': 'on' # Set to 'on' to activate the alert or 'off' to create it inactive } print(f"Adding new audit alert: {alert_name}") @@ -113,3 +114,6 @@ def execute_audit_alert_add(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/audit_alert/audit_alert_delete.py b/examples/audit_alert/audit_alert_delete.py index 770a150a..bdfe7d9a 100644 --- a/examples/audit_alert/audit_alert_delete.py +++ b/examples/audit_alert/audit_alert_delete.py @@ -90,6 +90,7 @@ def execute_audit_alert_delete(context: KeeperParams, **kwargs): alert_target = "alert_id" + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -108,3 +109,6 @@ def execute_audit_alert_delete(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/audit_alert/audit_alert_edit.py b/examples/audit_alert/audit_alert_edit.py index b4e78686..0e763bca 100644 --- a/examples/audit_alert/audit_alert_edit.py +++ b/examples/audit_alert/audit_alert_edit.py @@ -93,6 +93,7 @@ def execute_audit_alert_edit(context: KeeperParams, **kwargs): new_frequency = "1:hour" audit_events = ["login"] + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -115,3 +116,6 @@ def execute_audit_alert_edit(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/audit_alert/audit_alert_history.py b/examples/audit_alert/audit_alert_history.py new file mode 100644 index 00000000..9040fc41 --- /dev/null +++ b/examples/audit_alert/audit_alert_history.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_alert_history(context: KeeperParams, **kwargs): + """ + Execute audit alert history command. + + This function views the history of a specific audit alert + using the Keeper CLI command infrastructure. + """ + audit_alert_history_command = AuditAlertHistory() + + try: + audit_alert_history_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='View audit alert history using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_alert_history.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + alert_target = "Updated Test Alert" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'target': alert_target, + 'format': 'table' # Supported formats: table, json, csv + } + + print(f"Viewing audit alert history for: {alert_target}") + try: + execute_audit_alert_history(context, **kwargs) + print('Audit alert history completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/audit_alert/audit_alert_list.py b/examples/audit_alert/audit_alert_list.py index 76e8f3c9..fb314b98 100644 --- a/examples/audit_alert/audit_alert_list.py +++ b/examples/audit_alert/audit_alert_list.py @@ -88,6 +88,7 @@ def execute_audit_alert_list(context: KeeperParams, **kwargs): print(f'Config file {args.config} not found') sys.exit(1) + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -107,3 +108,6 @@ def execute_audit_alert_list(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/audit_alert/audit_alert_view.py b/examples/audit_alert/audit_alert_view.py index 9cf5fa4b..fd4f7573 100644 --- a/examples/audit_alert/audit_alert_view.py +++ b/examples/audit_alert/audit_alert_view.py @@ -90,6 +90,7 @@ def execute_audit_alert_view(context: KeeperParams, **kwargs): alert_target = "alert_id" + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -108,3 +109,6 @@ def execute_audit_alert_view(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/audit_report/audit_report.py b/examples/audit_report/audit_report.py new file mode 100644 index 00000000..17592332 --- /dev/null +++ b/examples/audit_report/audit_report.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_audit_report(context: KeeperParams, **kwargs): + """ + Execute audit report command. + + This function generates audit reports + using the Keeper CLI command infrastructure. + """ + audit_report_command = EnterpriseAuditReport() + + try: + audit_report_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate audit reports using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_report.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your audit report + report_type = "raw" # Can be: raw, dim, hour, day, week, month, span + report_format = "message" # message or fields (raw reports only) + created_filter = "last_7_days" # Filter by creation date + event_type = "login" # Audit event type filter + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'report_type': report_type, + 'report_format': report_format, + 'created': created_filter, + 'event_type': event_type, + 'limit': 100, # Maximum number of returned rows + 'order': 'desc', # Sort order: desc or asc + 'timezone': None, # Specific timezone + 'columns': ['username', 'audit_event_type'] if report_type != 'raw' else None, # Columns for aggregate reports + 'aggregates': ['occurrences'] if report_type != 'raw' else None, # Aggregated values - 'occurrences', 'first_created', 'last_created' + 'username': None, # Filter by username + 'to_username': None, # Filter by target username + 'from_username': None, # Filter by source username + 'record_uid': None, # Filter by record UID + 'shared_folder_uid': None, # Filter by shared folder UID + 'geo_location': None, # Filter by geo location + 'ip_address': None, # Filter by IP address + 'device_type': None, # Filter by device type + 'output': None, # Output file path + 'format': 'table' # Output format: table, csv, json + } + + print(f"Generating audit report:") + print(f" Report type: {report_type}") + print(f" Report format: {report_format}") + print(f" Created filter: {created_filter}") + print(f" Event type: {event_type}") + + try: + execute_audit_report(context, **kwargs) + print('Audit report generation completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/breachwatch/breachwatch_ignore.py b/examples/breachwatch/breachwatch_ignore.py index 83b2ec16..bf08f3e9 100644 --- a/examples/breachwatch/breachwatch_ignore.py +++ b/examples/breachwatch/breachwatch_ignore.py @@ -90,6 +90,7 @@ def execute_breachwatch_ignore(context: KeeperParams, **kwargs): record_uids = ["record_uid"] + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -108,3 +109,6 @@ def execute_breachwatch_ignore(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/breachwatch/breachwatch_list.py b/examples/breachwatch/breachwatch_list.py new file mode 100644 index 00000000..42f5c13d --- /dev/null +++ b/examples/breachwatch/breachwatch_list.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_breachwatch_list(context: KeeperParams, **kwargs): + """ + Execute breachwatch list command. + + This function lists all breached passwords in the vault + using the Keeper CLI command infrastructure. + """ + breachwatch_list_command = BreachWatchListCommand() + + try: + breachwatch_list_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List breached passwords using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python breachwatch_list.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Set all flags as either True for setting or None for False + show_all = True + owned_only = None + numbered = True + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'all': show_all, + 'owned': owned_only, + 'numbered': numbered + } + + print("Listing all breached passwords...") + try: + execute_breachwatch_list(context, **kwargs) + print('Breach watch list completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/breachwatch/breachwatch_scan.py b/examples/breachwatch/breachwatch_scan.py index 907e1e67..e6439711 100644 --- a/examples/breachwatch/breachwatch_scan.py +++ b/examples/breachwatch/breachwatch_scan.py @@ -90,6 +90,7 @@ def execute_breachwatch_scan(context: KeeperParams, **kwargs): record_uids = ["record_uid"] + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -108,3 +109,6 @@ def execute_breachwatch_scan(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/custom_record_type/create_custom_record_type.py b/examples/custom_record_type/create_custom_record_type.py index 1b3ec754..44449386 100644 --- a/examples/custom_record_type/create_custom_record_type.py +++ b/examples/custom_record_type/create_custom_record_type.py @@ -115,13 +115,17 @@ def create_custom_record_type( record_type_title = "New Custom Record Type" # Max 32 characters description = "An example custom record type created by the Keeper SDK" categories = ["custom", "example"] - field_names = ["login", "password", "url"] + field_names = ["login", "password", "url"] # For valid fields refer to record_types.FieldTypes and record_types.RecordFields in keepersdk.vault fields = [{"$ref": field} for field in field_names if field] + context = None try: - vault = login_to_keeper_with_config(args.config).vault - create_custom_record_type(vault, record_type_title, description, categories, fields) + context = login_to_keeper_with_config(args.config) + create_custom_record_type(context.vault, record_type_title, description, categories, fields) except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/custom_record_type/custom_record_type_info.py b/examples/custom_record_type/custom_record_type_info.py index 5ba7217a..31779a52 100644 --- a/examples/custom_record_type/custom_record_type_info.py +++ b/examples/custom_record_type/custom_record_type_info.py @@ -88,15 +88,17 @@ def execute_record_type_info(context: KeeperParams, **kwargs): print(f'Config file {args.config} not found') sys.exit(1) + context = None try: context = login_to_keeper_with_config(args.config) + kwargs = { + 'record_name': '*' + } + + success = execute_record_type_info(context, **kwargs) except Exception as e: print(f'Error: {str(e)}') sys.exit(1) - - kwargs = { - 'record_name': '*' - } - - success = execute_record_type_info(context, **kwargs) - sys.exit(0 if success else 1) \ No newline at end of file + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/custom_record_type/delete_custom_record_type.py b/examples/custom_record_type/delete_custom_record_type.py index 50257ee1..e83b9ae3 100644 --- a/examples/custom_record_type/delete_custom_record_type.py +++ b/examples/custom_record_type/delete_custom_record_type.py @@ -100,18 +100,18 @@ def delete_custom_record_type( print(f'Config file {args.config} not found') sys.exit(1) - record_type_id = 24375 - force = True + record_type_id = 000000 + force = True # True or False print(f"Note: This example will attempt to delete record type ID {record_type_id}") + context = None try: - vault = login_to_keeper_with_config(args.config).vault - success = delete_custom_record_type(vault, record_type_id, force) - - if not success: - sys.exit(1) - + context = login_to_keeper_with_config(args.config) + success = delete_custom_record_type(context.vault, record_type_id, force) except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/custom_record_type/download_record_types.py b/examples/custom_record_type/download_record_types.py index bf53c315..353935bf 100644 --- a/examples/custom_record_type/download_record_types.py +++ b/examples/custom_record_type/download_record_types.py @@ -95,6 +95,7 @@ def download_record_types(context: KeeperParams, **kwargs): print(f'Config file {args.config} not found') sys.exit(1) + context = None try: context = login_to_keeper_with_config(args.config) @@ -105,9 +106,9 @@ def download_record_types(context: KeeperParams, **kwargs): success = download_record_types(context, **kwargs) - if not success: - sys.exit(1) - except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/custom_record_type/edit_custom_record_type.py b/examples/custom_record_type/edit_custom_record_type.py index d989f6d2..8419d4f8 100644 --- a/examples/custom_record_type/edit_custom_record_type.py +++ b/examples/custom_record_type/edit_custom_record_type.py @@ -120,14 +120,18 @@ def edit_custom_record_type( title = "Updated Custom Record New" # Max 32 characters description = "An example custom record type created by the Keeper SDK" categories = ["custom", "example"] - field_names = ["login", "password", "url"] + field_names = ["login", "password", "url"] # For valid fields refer to record_types.FieldTypes and record_types.RecordFields in keepersdk.vault fields = [{"$ref": field} for field in field_names if field] print(f"Note: This example will attempt to edit record type ID {record_type_id}") + context = None try: - vault = login_to_keeper_with_config(args.config).vault - edit_custom_record_type(vault, record_type_id, title, fields, description, categories) + context = login_to_keeper_with_config(args.config) + edit_custom_record_type(context.vault, record_type_id, title, fields, description, categories) except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/custom_record_type/load_record_types.py b/examples/custom_record_type/load_record_types.py index 6d1433ab..16f8ee8e 100644 --- a/examples/custom_record_type/load_record_types.py +++ b/examples/custom_record_type/load_record_types.py @@ -100,16 +100,16 @@ def load_record_types(context: KeeperParams, **kwargs): print("You can create one first using the download_record_types.py example.") sys.exit(1) + context = None try: context = login_to_keeper_with_config(args.config) kwargs = { 'file': json_file } success = load_record_types(context, **kwargs) - - if not success: - sys.exit(1) - except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/enterprise_info/enterprise_info_node.py b/examples/enterprise_info/enterprise_info_node.py new file mode 100644 index 00000000..64f7b65f --- /dev/null +++ b/examples/enterprise_info/enterprise_info_node.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_info_node(context: KeeperParams, **kwargs): + """ + Execute enterprise info node command. + + This function displays enterprise node information + using the Keeper CLI command infrastructure. + """ + enterprise_info_node_command = EnterpriseInfoNodeCommand() + + try: + enterprise_info_node_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Display enterprise node information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_info_node.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + search_pattern = None + columns = "parent_node,user_count,team_count,role_count" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'pattern': search_pattern, + 'columns': columns, + 'format': 'table' # Supported formats: 'table', 'csv', 'json' + } + + print("Displaying enterprise node information...") + try: + execute_enterprise_info_node(context, **kwargs) + print('Enterprise node info completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_info/enterprise_info_role.py b/examples/enterprise_info/enterprise_info_role.py new file mode 100644 index 00000000..093d4fe0 --- /dev/null +++ b/examples/enterprise_info/enterprise_info_role.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_info_role(context: KeeperParams, **kwargs): + """ + Execute enterprise info role command. + + This function displays enterprise role information + using the Keeper CLI command infrastructure. + """ + enterprise_info_role_command = EnterpriseInfoRoleCommand() + + try: + enterprise_info_role_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Display enterprise role information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_info_role.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + search_pattern = None + columns = "visible_below,default_role,admin,node,user_count,team_count" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'pattern': search_pattern, + 'columns': columns, + 'format': 'table' # Supported formats: 'table', 'csv', 'json' + } + + print("Displaying enterprise role information...") + try: + execute_enterprise_info_role(context, **kwargs) + print('Enterprise role info completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_info/enterprise_info_team.py b/examples/enterprise_info/enterprise_info_team.py new file mode 100644 index 00000000..388d9c02 --- /dev/null +++ b/examples/enterprise_info/enterprise_info_team.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_info_team(context: KeeperParams, **kwargs): + """ + Execute enterprise info team command. + + This function displays enterprise team information + using the Keeper CLI command infrastructure. + """ + enterprise_info_team_command = EnterpriseInfoTeamCommand() + + try: + enterprise_info_team_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Display enterprise team information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_info_team.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + search_pattern = None + columns = "restricts,node,user_count,queued_user_count,role_count" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'pattern': search_pattern, + 'columns': columns, + 'format': 'table' # Supported formats: 'table', 'csv', 'json' + } + + print("Displaying enterprise team information...") + try: + execute_enterprise_info_team(context, **kwargs) + print('Enterprise team info completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_info/enterprise_info_tree.py b/examples/enterprise_info/enterprise_info_tree.py new file mode 100644 index 00000000..502e45c3 --- /dev/null +++ b/examples/enterprise_info/enterprise_info_tree.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_info_tree(context: KeeperParams, **kwargs): + """ + Execute enterprise info tree command. + + This function displays the enterprise tree structure + using the Keeper CLI command infrastructure. + """ + enterprise_info_tree_command = EnterpriseInfoTreeCommand() + + try: + result = enterprise_info_tree_command.execute(context=context, **kwargs) + if result: + print(result) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Display enterprise tree structure using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_info_tree.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_filter = None + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': node_filter, + 'verbose': True # True or None + } + + print("Displaying enterprise tree structure...") + try: + execute_enterprise_info_tree(context, **kwargs) + print('Enterprise tree display completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_info/enterprise_info_user.py b/examples/enterprise_info/enterprise_info_user.py new file mode 100644 index 00000000..8849e309 --- /dev/null +++ b/examples/enterprise_info/enterprise_info_user.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_info_user(context: KeeperParams, **kwargs): + """ + Execute enterprise info user command. + + This function displays enterprise user information + using the Keeper CLI command infrastructure. + """ + enterprise_info_user_command = EnterpriseInfoUserCommand() + + try: + enterprise_info_user_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Display enterprise user information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_info_user.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + search_pattern = None + columns = "name,status,transfer_status,node,team_count,role_count" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'pattern': search_pattern, + 'columns': columns, + 'format': 'table' # Supported formats: 'table', 'csv', 'json' + } + + print("Displaying enterprise user information...") + try: + execute_enterprise_info_user(context, **kwargs) + print('Enterprise user info completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_add.py b/examples/enterprise_node/enterprise_node_add.py new file mode 100644 index 00000000..890143a4 --- /dev/null +++ b/examples/enterprise_node/enterprise_node_add.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_add(context: KeeperParams, **kwargs): + """ + Execute enterprise node add command. + + This function creates a new enterprise node + using the Keeper CLI command infrastructure. + """ + enterprise_node_add_command = EnterpriseNodeAddCommand() + + try: + enterprise_node_add_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add new enterprise node using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_name = "New Test Node" + parent_node = "Keeper Security" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': [node_name], + 'parent': parent_node, + 'force': True # True or None + } + + print(f"Adding new enterprise node: {node_name}") + try: + execute_enterprise_node_add(context, **kwargs) + print('Enterprise node add completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_delete.py b/examples/enterprise_node/enterprise_node_delete.py new file mode 100644 index 00000000..661d5dd3 --- /dev/null +++ b/examples/enterprise_node/enterprise_node_delete.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_delete(context: KeeperParams, **kwargs): + """ + Execute enterprise node delete command. + + This function deletes an existing enterprise node + using the Keeper CLI command infrastructure. + """ + enterprise_node_delete_command = EnterpriseNodeDeleteCommand() + + try: + enterprise_node_delete_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete enterprise node using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_delete.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_name_or_id = "89444841422852" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': [node_name_or_id], + } + + print(f"Deleting enterprise node: {node_name_or_id}") + try: + execute_enterprise_node_delete(context, **kwargs) + print('Enterprise node delete completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_edit.py b/examples/enterprise_node/enterprise_node_edit.py new file mode 100644 index 00000000..33be9389 --- /dev/null +++ b/examples/enterprise_node/enterprise_node_edit.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_edit(context: KeeperParams, **kwargs): + """ + Execute enterprise node edit command. + + This function edits an existing enterprise node + using the Keeper CLI command infrastructure. + """ + enterprise_node_edit_command = EnterpriseNodeEditCommand() + + try: + enterprise_node_edit_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Edit enterprise node using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_edit.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_id = "New Test Node" + new_node_name = "Updated Node" + parent_node = "Keeper Security" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': node_id, + 'displayname': new_node_name, + 'parent': parent_node + } + + print(f"Editing enterprise node: {node_id} -> {new_node_name}") + try: + execute_enterprise_node_edit(context, **kwargs) + print('Enterprise node edit completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_invite_email.py b/examples/enterprise_node/enterprise_node_invite_email.py new file mode 100644 index 00000000..e687b298 --- /dev/null +++ b/examples/enterprise_node/enterprise_node_invite_email.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_invite_email(context: KeeperParams, **kwargs): + """ + Execute enterprise node invite email command. + + This function sends invitation emails from an enterprise node + using the Keeper CLI command infrastructure. + """ + enterprise_node_invite_command = EnterpriseNodeInviteCommand() + + try: + enterprise_node_invite_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Send invitation email from enterprise node using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_invite_email.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_name_or_id = "89444841422848" + invite_email = "test@test.com" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': node_name_or_id, + 'email': [invite_email], + 'force': True # True or None + } + + print(f"Sending invitation email from enterprise node: {node_name_or_id}") + print(f"Invitation email: {invite_email}") + try: + execute_enterprise_node_invite_email(context, **kwargs) + print('Enterprise node invite email completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_set_logo.py b/examples/enterprise_node/enterprise_node_set_logo.py new file mode 100644 index 00000000..84071b28 --- /dev/null +++ b/examples/enterprise_node/enterprise_node_set_logo.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_set_logo(context: KeeperParams, **kwargs): + """ + Execute enterprise node set logo command. + + This function sets the logo for an enterprise node + using the Keeper CLI command infrastructure. + """ + enterprise_node_set_logo_command = EnterpriseNodeSetLogoCommand() + + try: + enterprise_node_set_logo_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Set logo for enterprise node using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_set_logo.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_name = "New Test Node" + logo_file = "company_logo.png" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': node_name, + 'logo_file': logo_file + } + + print(f"Setting logo for enterprise node: {node_name}") + print(f"Logo file: {logo_file}") + try: + execute_enterprise_node_set_logo(context, **kwargs) + print('Enterprise node set logo completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_view.py b/examples/enterprise_node/enterprise_node_view.py new file mode 100644 index 00000000..f4163175 --- /dev/null +++ b/examples/enterprise_node/enterprise_node_view.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_view(context: KeeperParams, **kwargs): + """ + Execute enterprise node view command. + + This function displays enterprise node information + using the Keeper CLI command infrastructure. + """ + enterprise_node_view_command = EnterpriseNodeViewCommand() + + try: + enterprise_node_view_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='View enterprise node information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_view.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_name = "New Test Node" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': node_name, + 'format': 'table' # Supported formats: 'table', 'csv', 'json' + } + + print(f"Viewing enterprise node information: {node_name}") + try: + execute_enterprise_node_view(context, **kwargs) + print('Enterprise node view completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_node/enterprise_node_wipe_out.py b/examples/enterprise_node/enterprise_node_wipe_out.py new file mode 100644 index 00000000..f117c7ce --- /dev/null +++ b/examples/enterprise_node/enterprise_node_wipe_out.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_node_wipe_out(context: KeeperParams, **kwargs): + """ + Execute enterprise node wipe out command. + + This function wipes out an enterprise node + using the Keeper CLI command infrastructure. + """ + enterprise_node_wipe_out_command = EnterpriseNodeWipeOutCommand() + + try: + enterprise_node_wipe_out_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Wipe out enterprise node using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_node_wipe_out.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + node_name = "New Test Node" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'node': node_name + } + + print(f"Wiping out enterprise node: {node_name}") + print("WARNING: This operation is destructive and cannot be undone!") + try: + execute_enterprise_node_wipe_out(context, **kwargs) + print('Enterprise node wipe out completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_add.py b/examples/enterprise_role/enterprise_role_add.py new file mode 100644 index 00000000..de4be22f --- /dev/null +++ b/examples/enterprise_role/enterprise_role_add.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_add(context: KeeperParams, **kwargs): + """ + Execute enterprise role add command. + + This function creates a new enterprise role + using the Keeper CLI command infrastructure. + """ + enterprise_role_add_command = EnterpriseRoleAddCommand() + + try: + enterprise_role_add_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add new enterprise role using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + role_name = "Test Role" + new_user_inherit = "on" # on or off + visible_below = "off" # on or off + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': [role_name], + 'new_user': new_user_inherit, + 'visible_below': visible_below, + 'force': True # True or None + } + + print(f"Adding new enterprise role: {role_name}") + try: + execute_enterprise_role_add(context, **kwargs) + print('Enterprise role add completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_admin.py b/examples/enterprise_role/enterprise_role_admin.py new file mode 100644 index 00000000..bfdb0417 --- /dev/null +++ b/examples/enterprise_role/enterprise_role_admin.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_admin(context: KeeperParams, **kwargs): + """ + Execute enterprise role admin command. + + This function manages admin privileges for an enterprise role + using the Keeper CLI command infrastructure. + """ + enterprise_role_admin_command = EnterpriseRoleAdminCommand() + + try: + enterprise_role_admin_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Manage enterprise role admin privileges using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_admin.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + role_name = "Test Role" + node_name = "New Test Node" + privileges = ["MANAGE_TEAMS"] + cascade = "on" # on or off + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': role_name, + 'add_admin': node_name, + 'add_privilege': privileges, + 'cascade': cascade + } + + print(f"Managing admin privileges for enterprise role: {role_name}") + try: + execute_enterprise_role_admin(context, **kwargs) + print('Enterprise role admin completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_copy.py b/examples/enterprise_role/enterprise_role_copy.py new file mode 100644 index 00000000..739c6b67 --- /dev/null +++ b/examples/enterprise_role/enterprise_role_copy.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_copy(context: KeeperParams, **kwargs): + """ + Execute enterprise role copy command. + + This function copies an existing enterprise role with its enforcements + using the Keeper CLI command infrastructure. + """ + enterprise_role_copy_command = EnterpriseRoleCopyCommand() + + try: + enterprise_role_copy_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Copy enterprise role using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_copy.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + source_role = "Test Role" + target_node = "New Test Node" + new_role_name = "Test Role Copy" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': source_role, + 'node': target_node, + 'displayname': new_role_name + } + + print(f"Copying enterprise role: {source_role} to {new_role_name}") + try: + execute_enterprise_role_copy(context, **kwargs) + print('Enterprise role copy completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_delete.py b/examples/enterprise_role/enterprise_role_delete.py new file mode 100644 index 00000000..a508422a --- /dev/null +++ b/examples/enterprise_role/enterprise_role_delete.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_delete(context: KeeperParams, **kwargs): + """ + Execute enterprise role delete command. + + This function deletes a specific enterprise role + using the Keeper CLI command infrastructure. + """ + enterprise_role_delete_command = EnterpriseRoleDeleteCommand() + + try: + enterprise_role_delete_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete enterprise role using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_delete.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + role_name = "Test Role" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': [role_name] + } + + print(f"Deleting enterprise role: {role_name}") + try: + execute_enterprise_role_delete(context, **kwargs) + print('Enterprise role delete completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_edit.py b/examples/enterprise_role/enterprise_role_edit.py new file mode 100644 index 00000000..a534917f --- /dev/null +++ b/examples/enterprise_role/enterprise_role_edit.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_edit(context: KeeperParams, **kwargs): + """ + Execute enterprise role edit command. + + This function edits an existing enterprise role + using the Keeper CLI command infrastructure. + """ + enterprise_role_edit_command = EnterpriseRoleEditCommand() + + try: + enterprise_role_edit_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Edit enterprise role using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_edit.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + role_name = "Test Role" + new_name = "Updated Role" + new_user_inherit = "off" # on or off + visible_below = "on" # on or off + parent_node = "Keeper Security" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': role_name, + 'parent': parent_node, + 'displayname': new_name, + 'new_user': new_user_inherit, + 'visible_below': visible_below + } + + print(f"Editing enterprise role: {role_name}") + try: + execute_enterprise_role_edit(context, **kwargs) + print('Enterprise role edit completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_membership.py b/examples/enterprise_role/enterprise_role_membership.py new file mode 100644 index 00000000..9d82ccbd --- /dev/null +++ b/examples/enterprise_role/enterprise_role_membership.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_membership(context: KeeperParams, **kwargs): + """ + Execute enterprise role membership command. + + This function manages membership for an enterprise role + using the Keeper CLI command infrastructure. + """ + enterprise_role_membership_command = EnterpriseRoleMembershipCommand() + + try: + enterprise_role_membership_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Manage enterprise role membership using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_membership.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + role_name = "Test Role" + users_to_add = ["test@test.com"] + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': role_name, + 'add_user': users_to_add + } + + print(f"Managing membership for enterprise role: {role_name}") + try: + execute_enterprise_role_membership(context, **kwargs) + print('Enterprise role membership completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_role/enterprise_role_view.py b/examples/enterprise_role/enterprise_role_view.py new file mode 100644 index 00000000..b363e64e --- /dev/null +++ b/examples/enterprise_role/enterprise_role_view.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_role_view(context: KeeperParams, **kwargs): + """ + Execute enterprise role view command. + + This function views the details of a specific enterprise role + using the Keeper CLI command infrastructure. + """ + enterprise_role_view_command = EnterpriseRoleViewCommand() + + try: + enterprise_role_view_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='View enterprise role details using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_role_view.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + role_name = "Test Role" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'role': role_name, + 'verbose': True # True or None + } + + print(f"Viewing enterprise role details for: {role_name}") + try: + execute_enterprise_role_view(context, **kwargs) + print('Enterprise role view completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_team/enterprise_team_add.py b/examples/enterprise_team/enterprise_team_add.py new file mode 100644 index 00000000..7c9783de --- /dev/null +++ b/examples/enterprise_team/enterprise_team_add.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_team_add(context: KeeperParams, **kwargs): + """ + Execute enterprise team add command. + + This function creates a new enterprise team + using the Keeper CLI command infrastructure. + """ + enterprise_team_add_command = EnterpriseTeamAddCommand() + + try: + enterprise_team_add_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add new enterprise team using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_team_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + team = "New Test Team" + parent_node = "Keeper Security" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'team': [team], + 'parent': parent_node, + 'force': None # Set as True or None + } + + print(f"Adding new enterprise team: {team}") + try: + execute_enterprise_team_add(context, **kwargs) + print('Enterprise team add completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_team/enterprise_team_delete.py b/examples/enterprise_team/enterprise_team_delete.py new file mode 100644 index 00000000..0bbc1e1d --- /dev/null +++ b/examples/enterprise_team/enterprise_team_delete.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_team_delete(context: KeeperParams, **kwargs): + """ + Execute enterprise team delete command. + + This function deletes an existing enterprise team + using the Keeper CLI command infrastructure. + """ + enterprise_team_delete_command = EnterpriseTeamDeleteCommand() + + try: + enterprise_team_delete_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete enterprise team using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_team_delete.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + team_id = "xxx-yyy-zzz" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'team': [team_id] + } + + print(f"Deleting enterprise team: {team_id}") + try: + execute_enterprise_team_delete(context, **kwargs) + print('Enterprise team delete completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_team/enterprise_team_edit.py b/examples/enterprise_team/enterprise_team_edit.py new file mode 100644 index 00000000..9521b93c --- /dev/null +++ b/examples/enterprise_team/enterprise_team_edit.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_team_edit(context: KeeperParams, **kwargs): + """ + Execute enterprise team edit command. + + This function edits an existing enterprise team + using the Keeper CLI command infrastructure. + """ + enterprise_team_edit_command = EnterpriseTeamEditCommand() + + try: + enterprise_team_edit_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Edit enterprise team using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_team_edit.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + team = "team_id" + new_team = "team_name" + parent_node = "Node" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'team': [team], + 'displayname': new_team, + 'parent': parent_node + } + + print(f"Editing enterprise team: {team} -> {new_team}") + try: + execute_enterprise_team_edit(context, **kwargs) + print('Enterprise team edit completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_team/enterprise_team_membership.py b/examples/enterprise_team/enterprise_team_membership.py new file mode 100644 index 00000000..6b473aa0 --- /dev/null +++ b/examples/enterprise_team/enterprise_team_membership.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_team_membership(context: KeeperParams, **kwargs): + """ + Execute enterprise team membership command. + + This function manages membership of an enterprise team + using the Keeper CLI command infrastructure. + """ + enterprise_team_membership_command = EnterpriseTeamMembershipCommand() + + try: + enterprise_team_membership_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Manage enterprise team membership using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python _enterprise_team_membership.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + team_name = "Test Team" + add_user_email = "user@example.com" + add_role_name = "Test Role" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'team': [team_name], + 'add_user': [add_user_email], + 'add_role': [add_role_name], + 'force': True # Set as None to keep false + } + + print(f"Managing membership for enterprise team: {team_name}") + print(f"Adding user: {add_user_email}") + print(f"Adding role: {add_role_name}") + try: + execute_enterprise_team_membership(context, **kwargs) + print('Enterprise team membership management completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_team/enterprise_team_view.py b/examples/enterprise_team/enterprise_team_view.py new file mode 100644 index 00000000..623f2622 --- /dev/null +++ b/examples/enterprise_team/enterprise_team_view.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_team_view(context: KeeperParams, **kwargs): + """ + Execute enterprise team view command. + + This function displays enterprise team information + using the Keeper CLI command infrastructure. + """ + enterprise_team_view_command = EnterpriseTeamViewCommand() + + try: + enterprise_team_view_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='View enterprise team information using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_team_view.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + team_name_or_id = "New" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'team': team_name_or_id, + 'format': 'table' # Supported formats: 'table', 'csv', 'json' + } + + print(f"Viewing enterprise team information: {team_name_or_id}") + try: + execute_enterprise_team_view(context, **kwargs) + print('Enterprise team view completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_user/enterprise_user_action.py b/examples/enterprise_user/enterprise_user_action.py new file mode 100644 index 00000000..e00b7271 --- /dev/null +++ b/examples/enterprise_user/enterprise_user_action.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_user_action(context: KeeperParams, **kwargs): + """ + Execute enterprise user action command. + + This function performs actions on enterprise users (lock, unlock, expire, etc.) + using the Keeper CLI command infrastructure. + """ + enterprise_user_action_command = EnterpriseUserActionCommand() + + try: + enterprise_user_action_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Perform actions on enterprise users using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_user_action.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + user_email = "test@test.com" + action_type = "disable_2fa" # Options: expire, extend, lock, unlock, disable_2fa + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'email': [user_email], + action_type: True + } + + print(f"Performing action '{action_type}' on enterprise user: {user_email}") + try: + execute_enterprise_user_action(context, **kwargs) + print('Enterprise user action completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_user/enterprise_user_add.py b/examples/enterprise_user/enterprise_user_add.py new file mode 100644 index 00000000..ae129d33 --- /dev/null +++ b/examples/enterprise_user/enterprise_user_add.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_user_add(context: KeeperParams, **kwargs): + """ + Execute enterprise user add command. + + This function creates a new enterprise user + using the Keeper CLI command infrastructure. + """ + enterprise_user_add_command = EnterpriseUserAddCommand() + + try: + enterprise_user_add_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Add new enterprise user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_user_add.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + user_email = "test@test.com" + full_name = "Test User" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'email': [user_email], + 'full_name': full_name, + 'hide_shared_folders': 'off' # 'on' or 'off' + } + + print(f"Adding new enterprise user: {user_email}") + try: + execute_enterprise_user_add(context, **kwargs) + print('Enterprise user add completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_user/enterprise_user_alias.py b/examples/enterprise_user/enterprise_user_alias.py new file mode 100644 index 00000000..5afa7815 --- /dev/null +++ b/examples/enterprise_user/enterprise_user_alias.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_user_alias(context: KeeperParams, **kwargs): + """ + Execute enterprise user alias command. + + This function manages aliases for enterprise users + using the Keeper CLI command infrastructure. + """ + enterprise_user_alias_command = EnterpriseUserAliasCommand() + + try: + enterprise_user_alias_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Manage enterprise user aliases using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_user_alias.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + user_email = "test@test.com" + alias_email = "test" + action_type = "add" # Options: add, remove + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + if action_type == "add": + kwargs = { + 'email': user_email, + 'add_alias': alias_email + } + print(f"Adding alias '{alias_email}' to user: {user_email}") + else: + kwargs = { + 'email': user_email, + 'remove_alias': alias_email + } + print(f"Removing alias '{alias_email}' from user: {user_email}") + + try: + execute_enterprise_user_alias(context, **kwargs) + print('Enterprise user alias completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_user/enterprise_user_delete.py b/examples/enterprise_user/enterprise_user_delete.py new file mode 100644 index 00000000..11701c13 --- /dev/null +++ b/examples/enterprise_user/enterprise_user_delete.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_user_delete(context: KeeperParams, **kwargs): + """ + Execute enterprise user delete command. + + This function deletes a specific enterprise user + using the Keeper CLI command infrastructure. + """ + enterprise_user_delete_command = EnterpriseUserDeleteCommand() + + try: + enterprise_user_delete_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete enterprise user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_user_delete.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + user_email = "user@example.com" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'email': [user_email], + 'force': True # Set as None to keep as false + } + + print(f"Deleting enterprise user: {user_email}") + try: + execute_enterprise_user_delete(context, **kwargs) + print('Enterprise user delete completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/enterprise_user/enterprise_user_edit.py b/examples/enterprise_user/enterprise_user_edit.py new file mode 100644 index 00000000..a2682849 --- /dev/null +++ b/examples/enterprise_user/enterprise_user_edit.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_user_edit(context: KeeperParams, **kwargs): + """ + Execute enterprise user edit command. + + This function edits an existing enterprise user + using the Keeper CLI command infrastructure. + """ + enterprise_user_edit_command = EnterpriseUserEditCommand() + + try: + enterprise_user_edit_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Edit enterprise user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_user_edit.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + user_email = "test@test.com" + new_full_name = "Test User" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'email': [user_email], + 'full_name': new_full_name, + 'hide_shared_folders': 'off' # 'on' or 'off' + } + + print(f"Editing enterprise user: {user_email}") + try: + execute_enterprise_user_edit(context, **kwargs) + print('Enterprise user edit completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_user/enterprise_user_view.py b/examples/enterprise_user/enterprise_user_view.py new file mode 100644 index 00000000..679f1f44 --- /dev/null +++ b/examples/enterprise_user/enterprise_user_view.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_enterprise_user_view(context: KeeperParams, **kwargs): + """ + Execute enterprise user view command. + + This function views the details of a specific enterprise user + using the Keeper CLI command infrastructure. + """ + enterprise_user_view_command = EnterpriseUserViewCommand() + + try: + enterprise_user_view_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='View enterprise user details using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python enterprise_user_view.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + user_email = "test@test.com" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'team': user_email, + 'verbose': True # Set as None to keep as false + } + + print(f"Viewing enterprise user details for: {user_email}") + try: + execute_enterprise_user_view(context, **kwargs) + print('Enterprise user view completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/folder/share_folder.py b/examples/folder/share_folder.py index 270b4eb2..6c7ca676 100644 --- a/examples/folder/share_folder.py +++ b/examples/folder/share_folder.py @@ -126,12 +126,13 @@ def share_folder_with_user( folder_uid = "t5C4bl3iWmOPWugaWGaMIQ" user_email = "example@example.com" - manage_records = 'on' - manage_users = 'off' - action = 'grant' + manage_records = 'on' # 'on' or 'off' + manage_users = 'off' # 'on' or 'off' + action = 'grant' # 'grant' or 'remove' with grant being default if skipped print(f"Note: This example will attempt to share folder '{folder_uid}' with '{user_email}'") + context = None try: context = login_to_keeper_with_config(args.config) success = share_folder_with_user( @@ -148,4 +149,7 @@ def share_folder_with_user( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/importing_exporting/apply_membership.py b/examples/importing_exporting/apply_membership.py new file mode 100644 index 00000000..6a96729c --- /dev/null +++ b/examples/importing_exporting/apply_membership.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_apply_membership(context: KeeperParams, **kwargs): + """ + Execute apply membership command. + + This function applies shared folder membership data + using the Keeper CLI command infrastructure. + """ + apply_command = ApplyMembershipCommand() + + try: + apply_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Apply shared folder membership using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python apply_membership.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your membership application + input_file = "shared_folder_membership.json" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'name': input_file, + 'full_sync': None # Update and remove membership also, can be set to True + } + + print(f"Applying membership from file: {input_file}") + try: + execute_apply_membership(context, **kwargs) + print('Membership application completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/importing_exporting/download_membership.py b/examples/importing_exporting/download_membership.py new file mode 100644 index 00000000..a62e0e2e --- /dev/null +++ b/examples/importing_exporting/download_membership.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_download_membership(context: KeeperParams, **kwargs): + """ + Execute download membership command. + + This function downloads shared folder membership data + using the Keeper CLI command infrastructure. + """ + download_command = DownloadMembershipCommand() + + try: + download_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Download shared folder membership using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python download_membership.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your membership download + source = "keeper" # Membership source: keeper, lastpass, thycotic + output_file = "shared_folder_membership.json" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'source': source, + 'name': output_file, + 'permissions': None, # Force shared folder permissions: manage (U)sers, manage (R)ecords + 'restrictions': None, # Force shared folder restrictions: manage (U)sers, manage (R)ecords + 'folders_only': None, # Download shared folders only, skip teams + 'sub_folder': None # Shared sub-folder handling: ignore, flatten + } + + print(f"Downloading membership from source: {source}") + print(f"Output file: {output_file}") + try: + execute_download_membership(context, **kwargs) + print('Membership download completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/importing_exporting/export_data.py b/examples/importing_exporting/export_data.py new file mode 100644 index 00000000..0b8c6f1c --- /dev/null +++ b/examples/importing_exporting/export_data.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_export_data(context: KeeperParams, **kwargs): + """ + Execute export data command. + + This function exports data from the Keeper vault + using the Keeper CLI command infrastructure. + """ + export_command = ExportCommand() + + try: + export_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Export data from Keeper vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python export_data.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your export + export_format = "json" # Can be csv, json, etc. + output_file = "exported_vault.json" # Replace with your desired output file + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + # Flags can be set as None for false and True for true + kwargs = { + 'format': export_format, + 'output': output_file, + 'max_size': 100, # Maximum file size in MB + 'max_records': 10000, # Maximum number of records + 'regex': None, # Optional regex filter + 'only_password': None, + 'display': None, + 'title': True, + 'notes': True, + 'custom': True, + 'type': True, + 'folders': True, + 'attachments': None + } + + print(f"Exporting data to: {output_file}") + print(f"Export format: {export_format}") + try: + execute_export_data(context, **kwargs) + print('Data export completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/importing_exporting/import_data.py b/examples/importing_exporting/import_data.py new file mode 100644 index 00000000..0a470283 --- /dev/null +++ b/examples/importing_exporting/import_data.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_import_data(context: KeeperParams, **kwargs): + """ + Execute import data command. + + This function imports data into the Keeper vault + using the Keeper CLI command infrastructure. + """ + import_command = ImportCommand() + + try: + import_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Import data into Keeper vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python import_data.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your import + import_source = "csv" # Can be csv, json, keepass, etc. + import_file = "import_data.csv" # Replace with your import file + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + # Flags can be set as None for false and True for true + kwargs = { + 'source': import_source, + 'name': import_file, + 'display_csv': None, + 'overwrite': None, + 'login_replace': None, + 'ignore_csv': None, + 'skip_errors': None, + 'format': 'json', + 'share_existing': None + } + + print(f"Importing data from: {import_file}") + print(f"Source format: {import_source}") + try: + execute_import_data(context, **kwargs) + print('Data import completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/one_time_share/create_one_time_share.py b/examples/one_time_share/create_one_time_share.py index a773dac9..83caeef6 100644 --- a/examples/one_time_share/create_one_time_share.py +++ b/examples/one_time_share/create_one_time_share.py @@ -87,9 +87,10 @@ def execute_one_time_share_create(context: KeeperParams, **kwargs): record_name = "record_name" expire_time = "1h" share_name = "share_name" - output_destination = "stdout" - is_editable = True + output_destination = "stdout" # 'stdout' for printing the link in console or 'clipboard' to copy it to clipboard + is_editable = True # True or None + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -114,3 +115,6 @@ def execute_one_time_share_create(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/one_time_share/list_one_time_shares.py b/examples/one_time_share/list_one_time_shares.py index 485f9a23..1ae31518 100644 --- a/examples/one_time_share/list_one_time_shares.py +++ b/examples/one_time_share/list_one_time_shares.py @@ -89,6 +89,7 @@ def execute_one_time_share_list(context: KeeperParams, **kwargs): verbose = None show_all = None + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -97,9 +98,9 @@ def execute_one_time_share_list(context: KeeperParams, **kwargs): kwargs = { 'record': record_name, - 'recursive': recursive, # If recursive is sent, it will be considered True regardless of value, unless set as None - 'verbose': verbose, # If verbose is sent, it will be considered True regardless of value, unless set as None - 'show_all': show_all # If show_all is sent, it will be considered True regardless of value, unless set as None + 'recursive': recursive, # If recursive is sent, it will be considered True regardless of value (True or False), unless set as None + 'verbose': verbose, # If verbose is sent, it will be considered True regardless of value (True or False), unless set as None + 'show_all': show_all # If show_all is sent, it will be considered True regardless of value (True or False), unless set as None } print(f"Listing one-time shares for record: {record_name}") @@ -110,3 +111,6 @@ def execute_one_time_share_list(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/one_time_share/remove_one_time_share.py b/examples/one_time_share/remove_one_time_share.py index 7eb6d678..8226c2d1 100644 --- a/examples/one_time_share/remove_one_time_share.py +++ b/examples/one_time_share/remove_one_time_share.py @@ -87,6 +87,7 @@ def execute_one_time_share_remove(context: KeeperParams, **kwargs): record_name = "record_name" share_id = "share_id" + context = None try: context = login_to_keeper_with_config(args.config) except Exception as e: @@ -107,5 +108,6 @@ def execute_one_time_share_remove(context: KeeperParams, **kwargs): except Exception as e: print(f'Error: {str(e)}') sys.exit(1) - - + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/add_record.py b/examples/record/add_record.py index af24d649..a494ca72 100644 --- a/examples/record/add_record.py +++ b/examples/record/add_record.py @@ -66,7 +66,7 @@ def add_record( folder and include additional metadata like URL and notes. """ try: - record = vault_record.PasswordRecord() + record = vault_record.PasswordRecord() # Other option is vault_record.TypedRecord() record.title = title record.login = login record.password = password @@ -122,10 +122,14 @@ def add_record( notes = "This is an example record created by the Keeper SDK" folder_uid = None + context = None try: - vault = login_to_keeper_with_config(args.config).vault - add_record(vault, title, login, password, url, notes, folder_uid) + context = login_to_keeper_with_config(args.config) + add_record(context.vault, title, login, password, url, notes, folder_uid) except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/delete_attachment.py b/examples/record/delete_attachment.py new file mode 100644 index 00000000..63aa6a87 --- /dev/null +++ b/examples/record/delete_attachment.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_delete_attachment(context: KeeperParams, **kwargs): + """ + Execute delete attachment command. + + This function deletes attachments from a record + using the Keeper CLI command infrastructure. + """ + delete_command = RecordDeleteAttachmentCommand() + + try: + delete_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Delete attachments from a record using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python delete_attachment.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - replace with actual record UID and attachment names + record_uid = "record_uid_or_path" + attachment_names = ["file1.txt", "file2.pdf"] # Replace with actual attachment names or IDs + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'record': record_uid, + 'name': attachment_names + } + + print(f"Deleting attachments from record: {record_uid}") + print(f"Attachments: {', '.join(attachment_names)}") + try: + execute_delete_attachment(context, **kwargs) + print('Attachment deletion completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/record/delete_record.py b/examples/record/delete_record.py index c7f905e0..525879ba 100644 --- a/examples/record/delete_record.py +++ b/examples/record/delete_record.py @@ -123,11 +123,12 @@ def find_record_by_title(vault: vault_online.VaultOnline, title: str) -> Optiona sys.exit(1) title_to_delete = "Test Record 1" - force_delete = True + force_delete = True # Set to True to skip confirmation prompt or None to send as False + context = None try: - vault = login_to_keeper_with_config(args.config).vault - + context = login_to_keeper_with_config(args.config) + vault = context.vault record_uid = find_record_by_title(vault, title_to_delete) if not record_uid: print(f"No record found with title: '{title_to_delete}'") @@ -142,4 +143,7 @@ def find_record_by_title(vault: vault_online.VaultOnline, title: str) -> Optiona except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/download_attachment.py b/examples/record/download_attachment.py new file mode 100644 index 00000000..0d7d6bd4 --- /dev/null +++ b/examples/record/download_attachment.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_download_attachment(context: KeeperParams, **kwargs): + """ + Execute download attachment command. + + This function downloads attachments from a record + using the Keeper CLI command infrastructure. + """ + download_command = RecordDownloadAttachmentCommand() + + try: + download_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Download record attachments using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python download_attachment.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - replace with actual record UID or path + record_uid = "record_uid_or_path" + output_dir = "./downloads" + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + kwargs = { + 'records': [record_uid], + 'out_dir': output_dir, + 'preserve_dir': False, + 'record_title': True, + 'recursive': False + } + + print(f"Downloading attachments from record: {record_uid}") + try: + execute_download_attachment(context, **kwargs) + print('Attachment download completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/record/get_command.py b/examples/record/get_command.py index c196dda8..eab723a1 100644 --- a/examples/record/get_command.py +++ b/examples/record/get_command.py @@ -66,7 +66,7 @@ def get( try: get_command = RecordGetCommand() kwargs = { - 'uid': uid, + 'uid': uid, # 'team'/'folder'/'record' can be used to specify type and will replace uid } get_command.execute(context=context, **kwargs) print('Details retrieved successfully!') @@ -98,10 +98,11 @@ def get( print(f'Config file {args.config} not found') sys.exit(1) - uid = "record_uid" + uid = "record_uid" # Replace with actual record/folder/team UID or path or title print(f"Note: This example will attempt to get details for record/folder/team '{uid}'") + context = None try: context = login_to_keeper_with_config(args.config) success = get( @@ -114,4 +115,7 @@ def get( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/list_records.py b/examples/record/list_records.py index ae817397..315915b6 100644 --- a/examples/record/list_records.py +++ b/examples/record/list_records.py @@ -102,10 +102,12 @@ def list_records( print(f'Config file {args.config} not found') sys.exit(1) + # Bool flags can be set to True or None (to be sent as False) show_details = True criteria = None record_type = None + context = None try: context = login_to_keeper_with_config(args.config) list_records( @@ -117,4 +119,7 @@ def list_records( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/share_record.py b/examples/record/share_record.py index 0e325b2f..c16f7dff 100644 --- a/examples/record/share_record.py +++ b/examples/record/share_record.py @@ -123,14 +123,16 @@ def share_record_with_user( print(f'Config file {args.config} not found') sys.exit(1) + # Bool flags can be set to True or None (to be sent as False) record_uid = "UkezdUGQoTOztfi5cGFJnQ" user_email = "example@example.com" can_edit = True can_share = False - action = 'grant' + action = 'grant' # 'grant', 'revoke', 'owner', 'cancel' or 'remove' print(f"Note: This example will attempt to share record '{record_uid}' with '{user_email}'") + context = None try: context = login_to_keeper_with_config(args.config) success = share_record_with_user( @@ -147,4 +149,7 @@ def share_record_with_user( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/update_record.py b/examples/record/update_record.py index d537a200..a93caea2 100644 --- a/examples/record/update_record.py +++ b/examples/record/update_record.py @@ -238,7 +238,7 @@ def update_record(vault: vault_online.VaultOnline, record_criteria: str, updates sys.exit(1) record_to_update_uid = "UkezdUGQoTOztfi5cGFJnQ" - record_type = None + record_type = None # Can be set to default or custom record type if needed record_version = None updates = { 'title': 'Updated Example Record', @@ -248,9 +248,10 @@ def update_record(vault: vault_online.VaultOnline, record_criteria: str, updates 'notes': 'This record has been updated by the Keeper SDK example' } + context = None try: - vault = login_to_keeper_with_config(args.config).vault - success = update_record(vault, record_to_update_uid, updates, record_type=record_type, record_version=record_version) + context = login_to_keeper_with_config(args.config) + success = update_record(context.vault, record_to_update_uid, updates, record_type=record_type, record_version=record_version) if success: print('\nRecord update completed successfully!') @@ -259,4 +260,7 @@ def update_record(vault: vault_online.VaultOnline, record_criteria: str, updates except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/upload_attachment.py b/examples/record/upload_attachment.py new file mode 100644 index 00000000..c2bbd201 --- /dev/null +++ b/examples/record/upload_attachment.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def execute_upload_attachment(context: KeeperParams, **kwargs): + """ + Execute upload attachment command. + + This function uploads attachments to a record + using the Keeper CLI command infrastructure. + """ + upload_command = RecordUploadAttachmentCommand() + + try: + upload_command.execute(context=context, **kwargs) + except Exception as e: + raise Exception(f'Error: {str(e)}') + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Upload attachments to a record using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python upload_attachment.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - replace with actual record UID and file paths + record_uid = "record_uid_or_path" + files_to_upload = ["file1.txt", "file2.pdf"] # Replace with actual file paths + + context = None + try: + context = login_to_keeper_with_config(args.config) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + + kwargs = { + 'record': record_uid, + 'file': files_to_upload + } + + print(f"Uploading attachments to record: {record_uid}") + print(f"Files: {', '.join(files_to_upload)}") + try: + execute_upload_attachment(context, **kwargs) + print('Attachment upload completed successfully') + sys.exit(0) + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/secrets_manager_app/create_secrets_manager_app.py b/examples/secrets_manager_app/create_secrets_manager_app.py index 065dd4bb..27b2ec13 100644 --- a/examples/secrets_manager_app/create_secrets_manager_app.py +++ b/examples/secrets_manager_app/create_secrets_manager_app.py @@ -101,15 +101,19 @@ def create_secrets_manager_app( sys.exit(1) app_name = "Secrets Manager App 1" - force = True + force = True # Set to True to overwrite if app with same name exists, set to None to send as False + context = None try: - vault = login_to_keeper_with_config(args.config).vault - result = create_secrets_manager_app(vault, app_name, force) + context = login_to_keeper_with_config(args.config) + result = create_secrets_manager_app(context.vault, app_name, force) if result is None: sys.exit(1) except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/get_secrets_manager_app.py b/examples/secrets_manager_app/get_secrets_manager_app.py index 0fae6c21..ed12f5db 100644 --- a/examples/secrets_manager_app/get_secrets_manager_app.py +++ b/examples/secrets_manager_app/get_secrets_manager_app.py @@ -140,13 +140,17 @@ def get_secrets_manager_app(vault: vault_online.VaultOnline, app_id: str): logger.info(f"Note: This example will attempt to get details for app ID '{app_id}'") + context = None try: - vault = login_to_keeper_with_config(args.config).vault - app_details = get_secrets_manager_app(vault, app_id) + context = login_to_keeper_with_config(args.config) + app_details = get_secrets_manager_app(context.vault, app_id) if app_details is None: sys.exit(1) except Exception as e: logger.error(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/list_secrets_manager_apps.py b/examples/secrets_manager_app/list_secrets_manager_apps.py index 8330457a..07a0cb74 100644 --- a/examples/secrets_manager_app/list_secrets_manager_apps.py +++ b/examples/secrets_manager_app/list_secrets_manager_apps.py @@ -115,10 +115,14 @@ def list_secrets_manager_apps(vault: vault_online.VaultOnline): logger.error(f'Config file {args.config} not found') sys.exit(1) + context = None try: - vault = login_to_keeper_with_config(args.config).vault - list_secrets_manager_apps(vault) + context = login_to_keeper_with_config(args.config) + list_secrets_manager_apps(context.vault) except Exception as e: logger.error(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/remove_secrets_manager_app.py b/examples/secrets_manager_app/remove_secrets_manager_app.py index 5b8f1178..5854b577 100644 --- a/examples/secrets_manager_app/remove_secrets_manager_app.py +++ b/examples/secrets_manager_app/remove_secrets_manager_app.py @@ -124,17 +124,18 @@ def remove_secrets_manager_app( sys.exit(1) uid_or_name = "Secrets Manager App 1" - force = True + force = True # Set to True to force removal if app has shared records, folders, or clients; set to None to send as False print(f"Note: This example will attempt to remove app '{uid_or_name}'") + context = None try: - vault = login_to_keeper_with_config(args.config).vault - removed_app = remove_secrets_manager_app(vault, uid_or_name, force) - - if removed_app is None: - sys.exit(1) + context = login_to_keeper_with_config(args.config) + removed_app = remove_secrets_manager_app(context.vault, uid_or_name, force) except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_app_add_record.py b/examples/secrets_manager_app/secrets_manager_app_add_record.py index 566d9060..d48dca7b 100644 --- a/examples/secrets_manager_app/secrets_manager_app_add_record.py +++ b/examples/secrets_manager_app/secrets_manager_app_add_record.py @@ -114,10 +114,11 @@ def add_secrets_to_app( app_id = "RlO6y-idGBqu1Ax2yUYXKw" secret_uids = ["YJAAssUpHCf-2Xfjnlw5cw"] - is_editable = False + is_editable = None # Set to True to make secrets editable, set to None to send as False print(f"Note: This example will attempt to add secrets to app ID '{app_id}'") + context = None try: context = login_to_keeper_with_config(args.config) success = add_secrets_to_app( @@ -132,4 +133,7 @@ def add_secrets_to_app( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_app_remove_record.py b/examples/secrets_manager_app/secrets_manager_app_remove_record.py index e8611b19..cb99fbb7 100644 --- a/examples/secrets_manager_app/secrets_manager_app_remove_record.py +++ b/examples/secrets_manager_app/secrets_manager_app_remove_record.py @@ -114,6 +114,7 @@ def remove_secrets_from_app( print(f"Note: This example will attempt to remove secrets from app ID '{app_id}'") + context = None try: context = login_to_keeper_with_config(args.config) success = remove_secrets_from_app( @@ -127,4 +128,7 @@ def remove_secrets_from_app( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_client_add.py b/examples/secrets_manager_app/secrets_manager_client_add.py index b3fc3aea..284c88e4 100644 --- a/examples/secrets_manager_app/secrets_manager_client_add.py +++ b/examples/secrets_manager_app/secrets_manager_client_add.py @@ -131,13 +131,14 @@ def add_client_to_app( app_id = "RlO6y-idGBqu1Ax2yUYXKw" client_name = "DemoClient" count = 1 - unlock_ip = False - first_access_expires_in = 60 + unlock_ip = None # Set to True to unlock IP, set to None to send as False + first_access_expires_in = 60 # Minutes until first access expires access_expire_in_min = None - return_tokens = True + return_tokens = True # Set to True to return tokens, set to None to send as False print(f"Note: This example will attempt to add a client to app ID '{app_id}'") + context = None try: context = login_to_keeper_with_config(args.config) success = add_client_to_app( @@ -156,4 +157,7 @@ def add_client_to_app( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/secrets_manager_client_remove.py b/examples/secrets_manager_app/secrets_manager_client_remove.py index 41e466b4..6200886b 100644 --- a/examples/secrets_manager_app/secrets_manager_client_remove.py +++ b/examples/secrets_manager_app/secrets_manager_client_remove.py @@ -115,10 +115,11 @@ def remove_client_from_app( app_id = "RlO6y-idGBqu1Ax2yUYXKw" client_names_or_ids = ["DemoClient"] - force = True + force = True # Set to True to skip confirmation prompts, set to None to send as False print(f"Note: This example will attempt to remove clients from app ID '{app_id}'") + context = None try: context = login_to_keeper_with_config(args.config) success = remove_client_from_app( @@ -133,4 +134,7 @@ def remove_client_from_app( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/share_secrets_manager_app.py b/examples/secrets_manager_app/share_secrets_manager_app.py index efafb990..9e94dbd4 100644 --- a/examples/secrets_manager_app/share_secrets_manager_app.py +++ b/examples/secrets_manager_app/share_secrets_manager_app.py @@ -107,10 +107,11 @@ def share_secrets_manager_app( app_id = "RlO6y-idGBqu1Ax2yUYXKw" user_email = "example@example.com" - is_admin = False + is_admin = None # Set to True to grant admin permissions, set to None to send as False print(f"Note: This example will attempt to share app ID '{app_id}'") + context = None try: context = login_to_keeper_with_config(args.config) success = share_secrets_manager_app( @@ -125,4 +126,7 @@ def share_secrets_manager_app( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/secrets_manager_app/unshare_secrets_manager_app.py b/examples/secrets_manager_app/unshare_secrets_manager_app.py index f74e241c..39520ba0 100644 --- a/examples/secrets_manager_app/unshare_secrets_manager_app.py +++ b/examples/secrets_manager_app/unshare_secrets_manager_app.py @@ -105,6 +105,7 @@ def unshare_secrets_manager_app( print(f"Note: This example will attempt to unshare app ID '{app_id}'") + context = None try: context = login_to_keeper_with_config(args.config) success = unshare_secrets_manager_app( @@ -118,4 +119,7 @@ def unshare_secrets_manager_app( except Exception as e: print(f'Error: {str(e)}') - sys.exit(1) \ No newline at end of file + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file From 9ef4a9434bd71c3215eb303fabbc18de2eb587f6 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 19 Sep 2025 16:28:12 +0530 Subject: [PATCH 29/44] Trash commands added and bugs fixed --- .../src/keepercli/commands/base.py | 1 + .../src/keepercli/commands/enterprise_node.py | 2 +- .../src/keepercli/commands/enterprise_team.py | 12 +- .../keepercli/commands/enterprise_utils.py | 4 + .../keepercli/commands/record_file_report.py | 182 ++++ .../src/keepercli/commands/trash.py | 733 ++++++++++++++++ .../src/keepercli/commands/vault_record.py | 266 +++++- .../src/keepercli/register_commands.py | 6 +- .../keepersdk/enterprise/batch_management.py | 2 +- .../src/keepersdk/vault/attachment.py | 1 - .../keepersdk/vault/record_type_management.py | 3 - .../src/keepersdk/vault/trash_management.py | 818 ++++++++++++++++++ 12 files changed, 2013 insertions(+), 17 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/record_file_report.py create mode 100644 keepercli-package/src/keepercli/commands/trash.py create mode 100644 keepersdk-package/src/keepersdk/vault/trash_management.py diff --git a/keepercli-package/src/keepercli/commands/base.py b/keepercli-package/src/keepercli/commands/base.py index ae464cae..a7bd07d5 100644 --- a/keepercli-package/src/keepercli/commands/base.py +++ b/keepercli-package/src/keepercli/commands/base.py @@ -197,6 +197,7 @@ def execute_args(self, context: KeeperParams, args, **kwargs): if not verb and self.default_verb: verb = self.default_verb + self.print_help(**kwargs) if verb in self.aliases: verb = self.aliases[verb] diff --git a/keepercli-package/src/keepercli/commands/enterprise_node.py b/keepercli-package/src/keepercli/commands/enterprise_node.py index d3211030..d06b9a34 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_node.py +++ b/keepercli-package/src/keepercli/commands/enterprise_node.py @@ -321,7 +321,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: is_isolated = set_isolated if isinstance(set_isolated, bool) else None nodes_to_update = [enterprise_management.NodeEdit( - node_id=x.node_id, name=display_name, parent_id=parent_id, restrict_visibility=is_isolated) + node_id=x.node_id, name=display_name, parent_id=parent_id if parent_id else x.parent_id, restrict_visibility=is_isolated) for x in node_list] batch = batch_management.BatchManagement(loader=context.enterprise_loader, logger=self) batch.modify_nodes(to_update=nodes_to_update) diff --git a/keepercli-package/src/keepercli/commands/enterprise_team.py b/keepercli-package/src/keepercli/commands/enterprise_team.py index 050426f4..a56733cc 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_team.py +++ b/keepercli-package/src/keepercli/commands/enterprise_team.py @@ -259,16 +259,15 @@ def execute(self, context: KeeperParams, **kwargs) -> None: restrict_edit: Optional[bool] = None r_edit = kwargs.get('restrict_edit') - if r_edit is not None: - restrict_edit = r_edit == 'on' + restrict_edit = r_edit == 'on' + restrict_share: Optional[bool] = None r_share = kwargs.get('restrict_share') - if r_share is not None: - restrict_share = r_share == 'on' + restrict_share = r_share == 'on' + restrict_view: Optional[bool] = None r_view = kwargs.get('restrict_view') - if r_view is not None: - restrict_view = r_view == 'on' + restrict_view = r_view == 'on' teams_to_edit = [enterprise_management.TeamEdit( team_uid=x.team_uid, name=team_name, node_id=parent_id, @@ -277,6 +276,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: batch = batch_management.BatchManagement(loader=context.enterprise_loader, logger=self) batch.modify_teams(to_update=teams_to_edit) + batch.apply() class EnterpriseTeamDeleteCommand(base.ArgparseCommand, enterprise_management.IEnterpriseManagementLogger): diff --git a/keepercli-package/src/keepercli/commands/enterprise_utils.py b/keepercli-package/src/keepercli/commands/enterprise_utils.py index 1a1f646e..aeee9a34 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_utils.py +++ b/keepercli-package/src/keepercli/commands/enterprise_utils.py @@ -52,6 +52,8 @@ def resolve_existing_nodes(enterprise_data: enterprise_types.IEnterpriseData, no n = nn[0] elif len(nn) >= 2: raise base.CommandError(f'Node name "{node_name}" is not unique') + elif isinstance(nn, enterprise_types.Node): + n = nn if n is None: raise base.CommandError(f'Node name "{node_name}" is not found') found_nodes[n.node_id] = n @@ -361,6 +363,8 @@ def resolve_existing_teams(e_data: enterprise_types.IEnterpriseData, t = tt[0] elif len(tt) >= 2: raise base.CommandError(f'Team name "{team_name}" is not unique. Use Team UID.') + elif isinstance(tt, enterprise_types.Team): + t = tt if t is None: missing_teams.append(team_name) continue diff --git a/keepercli-package/src/keepercli/commands/record_file_report.py b/keepercli-package/src/keepercli/commands/record_file_report.py new file mode 100644 index 00000000..eaa1cc91 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/record_file_report.py @@ -0,0 +1,182 @@ +import argparse +from typing import Dict, List, Any, Optional + +from keepersdk.vault import attachment, vault_record, record_facades +from keepersdk.authentication import endpoint +import requests + +from . import base +from .. import api +from ..helpers import report_utils +from ..params import KeeperParams + + +logger = api.get_logger() + + +class RecordFileReportCommand(base.ArgparseCommand): + """Command to generate a report of records with file attachments.""" + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='file-report', + parents=[base.report_output_parser], + description='List records with file attachments.' + ) + RecordFileReportCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '-d', '--try-download', + dest='try_download', + action='store_true', + help='Try downloading every attachment you have access to.' + ) + + def execute(self, context: KeeperParams, **kwargs) -> Any: + if not context.vault: + raise ValueError("Vault is not initialized.") + + try_download = kwargs.get('try_download', False) + headers = self._build_headers(try_download, kwargs.get('format')) + table = self._build_file_report_table(context.vault, try_download) + + return report_utils.dump_report_data( + table, + headers, + fmt=kwargs.get('format'), + filename=kwargs.get('output') + ) + + def _build_headers(self, try_download: bool, format_type: Optional[str]) -> List[str]: + """Build headers for the report based on options.""" + headers = ['title', 'record_uid', 'record_type', 'file_id', 'file_name', 'file_size'] + + if try_download: + headers.append('downloadable') + + if format_type != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + + return headers + + def _build_file_report_table(self, vault, try_download: bool) -> List[List[Any]]: + """Build the main report table with file attachment data.""" + table = [] + facade = record_facades.FileRefRecordFacade() + + for record_uid in vault.vault_data._records: + record = vault.vault_data.load_record(record_uid) + + if not self._record_has_files(record, facade): + continue + + # Test download accessibility if requested + download_statuses = {} + if try_download: + download_statuses = self._test_download_accessibility(vault, record_uid, record.title) + + # Add rows for this record's files + table.extend(self._add_record_file_rows(record, facade, download_statuses, vault)) + + return table + + def _record_has_files(self, record, facade) -> bool: + """Check if a record has file attachments or file references.""" + if isinstance(record, vault_record.PasswordRecord): + return bool(record.attachments) + elif isinstance(record, vault_record.TypedRecord): + facade.record = record + return bool(facade.file_ref) + return False + + def _test_download_accessibility(self, vault, record_uid: str, record_title: str) -> Dict[str, str]: + """Test download accessibility for all attachments in a record.""" + logger.info('Testing download accessibility for record: %s', record_title) + statuses = {} + + try: + downloads = list(attachment.prepare_attachment_download(vault, record_uid)) + for download in downloads: + status = self._test_single_download(download) + if status: + statuses[download.file_id] = status + except Exception as e: + logger.debug('Error preparing downloads for record %s: %s', record_uid, e) + + return statuses + + def _test_single_download(self, download) -> Optional[str]: + """Test download accessibility for a single attachment.""" + if not download.url: + return None + + try: + response = requests.get( + download.url, + proxies=endpoint.get_proxies(), + headers={"Range": "bytes=0-1"} + ) + return 'OK' if response.status_code in {200, 206} else str(response.status_code) + except Exception as e: + logger.debug('Download test failed for file %s: %s', download.file_id, e) + return None + + def _add_record_file_rows(self, record, facade, download_statuses: Dict[str, str], vault) -> List[List[Any]]: + """Add file rows for a specific record.""" + rows = [] + + if isinstance(record, vault_record.PasswordRecord): + rows.extend(self._add_password_record_rows(record, download_statuses)) + elif isinstance(record, vault_record.TypedRecord): + rows.extend(self._add_typed_record_rows(record, facade, download_statuses, vault)) + + return rows + + def _add_password_record_rows(self, record: vault_record.PasswordRecord, download_statuses: Dict[str, str]) -> List[List[Any]]: + """Add rows for password record attachments.""" + rows = [] + + for attachment in record.attachments: + row = [ + record.title, + record.record_uid, + '', # No record type for password records + attachment.id, + attachment.title or attachment.name, + attachment.size + ] + + if download_statuses: + row.append(download_statuses.get(attachment.id)) + + rows.append(row) + + return rows + + def _add_typed_record_rows(self, record: vault_record.TypedRecord, facade, download_statuses: Dict[str, str], vault) -> List[List[Any]]: + """Add rows for typed record file references.""" + rows = [] + facade.record = record + + for file_uid in facade.file_ref: + file_record = vault.vault_data.load_record(file_uid) + + if isinstance(file_record, vault_record.FileRecord): + row = [ + record.title, + record.record_uid, + record.record_type, + file_record.record_uid, + file_record.title or file_record.name, + file_record.size + ] + + if download_statuses: + row.append(download_statuses.get(file_record.record_uid)) + + rows.append(row) + + return rows diff --git a/keepercli-package/src/keepercli/commands/trash.py b/keepercli-package/src/keepercli/commands/trash.py new file mode 100644 index 00000000..0698b0eb --- /dev/null +++ b/keepercli-package/src/keepercli/commands/trash.py @@ -0,0 +1,733 @@ +import argparse +import datetime +import fnmatch +import json +import re +from typing import List, Dict, Any, Optional + +from . import base +from .. import api, prompt_utils +from ..helpers import report_utils, share_utils +from ..params import KeeperParams + +from keepersdk import utils +from keepersdk.proto import record_pb2 +from keepersdk.vault import trash_management +from keepersdk.vault.trash_management import TrashManagement + + +logger = api.get_logger() +STRING_LENGTH_LIMIT = 100 +CHUNK_SIZE_LIMIT = 900 +TRUNCATE_SUFFIX = '...' + + +class TrashCommand(base.GroupCommand): + """Main command class for trash management operations.""" + + def __init__(self): + super().__init__('Trash.') + self.register_command(TrashListCommand(), 'list') + self.register_command(TrashGetCommand(), 'get') + self.register_command(TrashRestoreCommand(), 'restore') + self.register_command(TrashUnshareCommand(), 'unshare') + self.register_command(TrashPurgeCommand(), 'purge') + self.default_verb = 'list' + + +class TrashListCommand(base.ArgparseCommand): + """Command to display a list of deleted records in the trash.""" + + def __init__(self): + parser = argparse.ArgumentParser( + prog='trash list', description='Displays a list of deleted records.', parents=[base.report_output_parser] + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command-specific arguments to the parser.""" + parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help="verbose output") + parser.add_argument('pattern', nargs='?', type=str, action='store', help='search pattern') + + def execute(self, context: KeeperParams, **kwargs): + """Execute the trash list command.""" + TrashManagement._ensure_deleted_records_loaded(context.vault) + + deleted_records = TrashManagement.get_deleted_records() + orphaned_records = TrashManagement.get_orphaned_records() + shared_folders = TrashManagement.get_shared_folders() + + if self._is_trash_empty(deleted_records, orphaned_records, shared_folders): + logger.info('Trash is empty') + return + + pattern = self._normalize_search_pattern(kwargs.get('pattern')) + title_pattern = self._create_title_pattern(pattern) + + headers = ['Folder UID', 'Record UID', 'Name', 'Record Type', 'Deleted At', 'Status'] + record_table = self._build_record_table(deleted_records, orphaned_records, pattern, title_pattern) + folder_table = self._build_folder_table(shared_folders, kwargs.get('verbose', False)) + + record_table.sort(key=lambda x: x[2].casefold()) + folder_table.sort(key=lambda x: x[2].casefold()) + all_records = record_table + folder_table + + return report_utils.dump_report_data( + all_records, headers, + fmt=kwargs.get('format'), + filename=kwargs.get('output'), + row_number=True + ) + + def _is_trash_empty(self, deleted_records: Dict, orphaned_records: Dict, shared_folders: Dict) -> bool: + """Check if trash is empty.""" + return (len(deleted_records) == 0 and + len(orphaned_records) == 0 and + len(shared_folders) == 0) + + def _normalize_search_pattern(self, pattern: Optional[str]) -> Optional[str]: + """Normalize search pattern (convert '*' to None).""" + if pattern == '*': + return None + return pattern + + def _create_title_pattern(self, pattern: Optional[str]) -> Optional[re.Pattern]: + """Safely compile regex pattern with length limits.""" + if len(pattern) > STRING_LENGTH_LIMIT: # Prevent ReDoS + logger.warning("Pattern too long, truncated") + pattern = pattern[:STRING_LENGTH_LIMIT] + + try: + return re.compile(fnmatch.translate(pattern), re.IGNORECASE) + except re.error as e: + logger.warning("Invalid pattern: %s", e) + return None + + def _build_record_table(self, deleted_records: Dict, orphaned_records: Dict, + pattern: Optional[str], title_pattern: Optional[re.Pattern]) -> List[List]: + """Build the record table for deleted and orphaned records.""" + record_table = [] + + # Process deleted records + self._add_records_to_table(deleted_records, False, pattern, title_pattern, record_table) + + # Process orphaned records + self._add_records_to_table(orphaned_records, True, pattern, title_pattern, record_table) + + return record_table + + def _add_records_to_table(self, records: Dict, is_shared: bool, pattern: Optional[str], + title_pattern: Optional[re.Pattern], record_table: List[List]) -> None: + """Add records to the table if they match the criteria.""" + for record in records.values(): + if self._should_include_record(record, pattern, title_pattern): + row = self._create_record_row(record, is_shared) + record_table.append(row) + + def _should_include_record(self, record: Dict, pattern: Optional[str], + title_pattern: Optional[re.Pattern]) -> bool: + """Check if record should be included based on search pattern.""" + if not pattern: + return True + + record_uid = record.get('record_uid') + if pattern == record_uid: + return True + + if title_pattern: + record_data_json = record.get('data_unencrypted') + try: + record_data = json.loads(record_data_json) + record_title = record_data.get('title', '') + return title_pattern.match(record_title) is not None + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.debug("Failed to parse record data: %s", e) + return False + + return False + + def _create_record_row(self, record: Dict, is_shared: bool) -> List: + """Create a table row for a record.""" + record_uid = record.get('record_uid') + record_data_json = record.get('data_unencrypted') + + try: + record_data = json.loads(record_data_json) if record_data_json else {} + record_title = record_data.get('title', '') + record_type = record_data.get('type', '') + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.debug("Failed to parse record data for row creation: %s", e) + record_title = 'Parse Error' + record_type = 'Unknown' + + if is_shared: + status = 'Share' + date_deleted = None + else: + status = 'Record' + date_deleted = self._get_deleted_date(record) + + return ['', record_uid, record_title, record_type, date_deleted, status] + + def _build_folder_table(self, shared_folders: Dict, verbose: bool) -> List[List]: + """Build the folder table for shared folders.""" + if not shared_folders: + return [] + + folders = shared_folders.get('folders', {}) + records = shared_folders.get('records', {}) + + if verbose: + return self._build_verbose_folder_table(folders, records) + else: + return self._build_summary_folder_table(folders, records) + + def _build_verbose_folder_table(self, folders: Dict, records: Dict) -> List[List]: + """Build verbose folder table showing individual records.""" + folder_table = [] + + for record in records.values(): + folder_uid = record.get('folder_uid') + record_uid = record.get('record_uid') + record_data_json = record.get('data_unencrypted') + + try: + record_data = json.loads(record_data_json) if record_data_json else {} + + if not record_data: + continue + + record_title = record_data.get('title', '') + record_type = record_data.get('type', '') + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.debug("Failed to parse folder record data: %s", e) + continue + + date_deleted = self._get_deleted_date(record) + + folder_table.append([ + folder_uid, record_uid, record_title, + record_type, date_deleted, 'Folder' + ]) + + return folder_table + + def _build_summary_folder_table(self, folders: Dict, records: Dict) -> List[List]: + """Build summary folder table showing folder counts.""" + folder_table = [] + record_counts = self._count_records_per_folder(records) + + for folder in folders.values(): + folder_uid = folder.get('folder_uid') + date_deleted = self._get_deleted_date(folder) + record_count = record_counts.get(folder_uid, 0) + + record_count_text = f'{record_count} record(s)' if record_count > 0 else None + folder_name = self._get_folder_name(folder, folder_uid) + + folder_table.append([ + folder_uid, record_count_text, folder_name, + '', date_deleted, 'Folder' + ]) + + return folder_table + + def _count_records_per_folder(self, records: Dict) -> Dict[str, int]: + """Count records per folder.""" + record_counts = {} + for record in records.values(): + folder_uid = record.get('folder_uid') + record_counts[folder_uid] = record_counts.get(folder_uid, 0) + 1 + return record_counts + + def _get_deleted_date(self, item: Dict) -> Optional[datetime.datetime]: + """Get deleted date from item with validation.""" + date_deleted_timestamp = item.get('date_deleted', 0) + if date_deleted_timestamp: + try: + # Validate timestamp type + if not isinstance(date_deleted_timestamp, (int, float)): + logger.debug("Invalid timestamp type: %s", type(date_deleted_timestamp)) + return None + + # Convert to seconds and validate range + timestamp_seconds = int(date_deleted_timestamp / 1000) + + # Check for reasonable date range (1970-2100) + if timestamp_seconds < 0 or timestamp_seconds > 4102444800: # Jan 1, 2100 + logger.debug("Timestamp out of range: %s", timestamp_seconds) + return None + + return datetime.datetime.fromtimestamp(timestamp_seconds) + except (ValueError, OSError, OverflowError) as e: + logger.debug("Invalid timestamp conversion: %s", e) + return None + return None + + def _get_folder_name(self, folder: Dict, folder_uid: str) -> str: + """Get folder name, falling back to UID if parsing fails.""" + try: + data_bytes = folder.get('data_unencrypted') + data_json = utils.base64_url_encode(data_bytes) + data = json.loads(data_json) + return data.get('name') or folder_uid + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.debug('Load folder data: %s', e) + return folder_uid + except Exception as e: + logger.debug('Load folder data: %s', e) + return folder_uid + + +class TrashGetCommand(base.ArgparseCommand): + """Command to get details of a deleted record.""" + + def __init__(self): + parser = argparse.ArgumentParser(prog='trash get', description='Get the details of a deleted record.') + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command-specific arguments to the parser.""" + parser.add_argument('record', action='store', help='Deleted record UID') + + def execute(self, context: KeeperParams, **kwargs): + """Execute the trash get command.""" + record_uid = kwargs.get('record') + if not record_uid: + logger.info('Record UID parameter is required') + return + + # Validate record UID format and length + if not isinstance(record_uid, str): + logger.info('Record UID must be a string') + return + + if len(record_uid) == 0 or len(record_uid) > STRING_LENGTH_LIMIT: + logger.info('Invalid record UID length') + return + + try: + record, is_shared = trash_management.get_trash_record(context.vault, record_uid) + except Exception as e: + logger.error('Error retrieving record: %s', e) + return + if not record: + logger.info('%s is not a valid deleted record UID', record_uid) + return + + record_data = self._parse_record_data(record) + if not record_data: + logger.info('Cannot restore record %s', record_uid) + return + + self._display_record_info(record_data) + + if is_shared: + self._display_share_info(context, record, record_uid) + + def _parse_record_data(self, record: Dict) -> Optional[Dict]: + """Parse record data from JSON with security validation.""" + record_data_json = record.get('data_unencrypted') + if not record_data_json: + return None + + try: + return json.loads(record_data_json) + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.debug("Failed to parse record data: %s", e) + return None + + def _display_record_info(self, record_data: Dict): + """Display basic record information.""" + title = record_data.get('title') + record_type = record_data.get('type') + + logger.info('{0:>21s}: {1}'.format('Title', title)) + logger.info('{0:>21s}: {1}'.format('Type', record_type)) + + self._display_record_fields(record_data.get('fields', {})) + + def _display_record_fields(self, fields: List[Dict]): + """Display record fields.""" + for field in fields: + field_name = self._get_field_name(field) + field_value = self._format_field_value(field.get('value')) + + if field_value: + logger.info('{0:>21s}: {1}'.format(field_name, field_value)) + + def _get_field_name(self, field: Dict) -> str: + """Get display name for field.""" + label = field.get('label') + if label and label != '': + return label + return field.get('type', '') + + def _format_field_value(self, value: Any) -> Optional[str]: + """Format field value for display.""" + if not value: + return None + + if isinstance(value, list): + value = '\n'.join(value) + + if len(value) > STRING_LENGTH_LIMIT: + value = value[:STRING_LENGTH_LIMIT-1] + TRUNCATE_SUFFIX + + return value + + def _display_share_info(self, context: KeeperParams, record: Dict, record_uid: str): + """Display share information for shared records.""" + if 'shares' not in record: + self._load_record_shares(context.vault, record, record_uid) + + if 'shares' in record and 'user_permissions' in record['shares']: + self._display_user_permissions(record['shares']['user_permissions'], context.username) + + def _load_record_shares(self, vault, record: Dict, record_uid: str): + """Load record shares if not already present.""" + record['shares'] = {} + shares = share_utils.get_record_shares(vault, [record_uid], True) + + if isinstance(shares, list): + record_shares = next( + (x.get('shares') for x in shares if x.get('record_uid') == record_uid), + None + ) + if isinstance(record_shares, dict): + record['shares'] = record_shares + + def _display_user_permissions(self, user_permissions: List[Dict], current_username: str): + """Display user permissions in sorted order.""" + sorted_permissions = self._sort_user_permissions(user_permissions) + + for index, permission in enumerate(sorted_permissions): + if permission.get('owner'): + continue + + username = permission['username'] + flags = self._get_permission_flags(permission) + self_flag = 'self' if username == current_username else '' + + header = 'Direct User Shares' if index == 0 else '' + logger.info('{0:>21s}: {1:<26s} ({2}) {3}'.format( + header, username, flags, self_flag + )) + + def _sort_user_permissions(self, permissions: List[Dict]) -> List[Dict]: + """Sort user permissions by priority.""" + return sorted(permissions, key=lambda p: ( + ' 1' if p.get('owner') else + ' 2' if p.get('editable') else + ' 3' if p.get('shareable') else + '' + ) + p.get('username', '')) + + def _get_permission_flags(self, permission: Dict) -> str: + """Get permission flags as a string.""" + flags = [] + + if permission.get('editable'): + flags.append('Can Edit') + + if permission.get('shareable'): + share_flag = 'Can Share' + if flags: + share_flag = ' & ' + share_flag + flags.append(share_flag) + + return ' '.join(flags) if flags else 'Read Only' + + +class TrashRestoreCommand(base.ArgparseCommand): + """Command to restore deleted records from trash.""" + + def __init__(self): + parser = argparse.ArgumentParser(prog='trash restore', description='Restores deleted records.') + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command-specific arguments to the parser.""" + parser.add_argument('-f', '--force', dest='force', action='store_true', + help='do not prompt for confirmation') + parser.add_argument('records', nargs='+', type=str, action='store', + help='Record UID or search pattern') + + def execute(self, context: KeeperParams, **kwargs): + """Execute the trash restore command.""" + records = self._validate_records_parameter(kwargs.get('records')) + if not records: + logger.info('records parameter is empty.') + return + + confirm_callback = self._create_confirm_callback(kwargs.get('force', False)) + trash_management.restore_trash_records(context.vault, records, confirm_callback) + + def _validate_records_parameter(self, records: Any) -> Optional[List[str]]: + """Validate and normalize records parameter with security checks.""" + if not isinstance(records, (tuple, list)): + return None + + # Check list size to prevent DoS + if len(records) > 10000: # Reasonable limit + logger.info('Too many records specified (max: 10000)') + return None + + validated_records = [] + for i, record in enumerate(records): + if self._is_valid_record(record, i + 1): + validated_records.append(record) + + return validated_records if validated_records else None + + def _is_valid_record(self, record: str, index: int) -> bool: + """Check if a single record is valid.""" + if not isinstance(record, str): + logger.info('Record %d must be a string', index) + return False + + # Validate UID format and length + if len(record) == 0 or len(record) > STRING_LENGTH_LIMIT: + logger.info('Record %d has invalid length', index) + return False + + return True + + def _create_confirm_callback(self, force: bool): + """Create confirmation callback based on force flag.""" + if force: + return None + + def confirm_callback(question): + return prompt_utils.user_choice(question, 'yn', default='n') + return confirm_callback + + +class TrashPurgeCommand(base.ArgparseCommand): + """Command to purge all records from trash.""" + + def __init__(self): + parser = argparse.ArgumentParser( + prog='trash purge', description='Removes all deleted record from the trash bin.' + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command-specific arguments to the parser.""" + parser.add_argument( + '-f', '--force', dest='force', action='store_true', help='do not prompt for confirmation' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the trash purge command.""" + if not kwargs.get('force'): + if not self._confirm_purge(): + return + + trash_management.purge_trash(context.vault) + + def _confirm_purge(self) -> bool: + """Confirm purge operation with user.""" + answer = prompt_utils.user_choice('Do you want to empty your Trash Bin?', 'yn', default='n') + if answer.lower() == 'y': + answer = 'yes' + return answer.lower() == 'yes' + + +class TrashUnshareCommand(base.ArgparseCommand): + """Command to remove shares from deleted records.""" + + def __init__(self): + parser = argparse.ArgumentParser(prog='trash unshare', description='Remove shares from deleted records.') + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command-specific arguments to the parser.""" + parser.add_argument( + '-f', '--force', dest='force', action='store_true', help='do not prompt for confirmation' + ) + parser.add_argument( + 'records', nargs='+', type=str, action='store', help='Record UID or search pattern. \"*\" for all records' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the trash unshare command.""" + records = self._validate_records_parameter(kwargs.get('records')) + if not records: + logger.info('records parameter is empty.') + return + + TrashManagement._ensure_deleted_records_loaded(context.vault) + orphaned_records = TrashManagement.get_orphaned_records() + + if not orphaned_records: + logger.info('Trash is empty') + return + + records_to_unshare = self._find_records_to_unshare(records, orphaned_records) + if not records_to_unshare: + logger.info('There are no records to unshare') + return + + if not self._confirm_unshare(kwargs.get('force', False), len(records_to_unshare)): + return + + self._remove_shares_from_records(context.vault, records_to_unshare) + + def _validate_records_parameter(self, records: Any) -> Optional[List[str]]: + """Validate and normalize records parameter with security checks.""" + if not isinstance(records, (tuple, list)): + return None + + # Check list size to prevent DoS + if len(records) > 10000: # Reasonable limit + logger.info('Too many records specified (max: 10000)') + return None + + validated_records = [] + for i, record in enumerate(records): + if self._is_valid_record(record, i + 1): + validated_records.append(record) + + return validated_records if validated_records else None + + def _is_valid_record(self, record: str, index: int) -> bool: + """Check if a single record is valid.""" + if len(record) == 0 or len(record) > STRING_LENGTH_LIMIT: + logger.info('Record %d has invalid length', index) + return False + + return True + + def _find_records_to_unshare(self, record_patterns: List[str], orphaned_records: Dict) -> List[str]: + """Find records to unshare based on patterns.""" + records_to_unshare = set() + + for pattern in record_patterns: + if pattern in orphaned_records: + records_to_unshare.add(pattern) + else: + self._add_matching_records(pattern, orphaned_records, records_to_unshare) + + return list(records_to_unshare) + + def _add_matching_records(self, pattern: str, orphaned_records: Dict, records_to_unshare: set): + """Add records matching the pattern to the unshare set.""" + if len(pattern) > STRING_LENGTH_LIMIT: # Prevent ReDoS + logger.warning("Pattern too long, truncated") + pattern = pattern[:STRING_LENGTH_LIMIT] + + try: + title_pattern = re.compile(fnmatch.translate(pattern), re.IGNORECASE) + except re.error as e: + raise base.CommandError("Invalid pattern: %s", e) + + for record_uid, record in orphaned_records.items(): + if record_uid in records_to_unshare: + continue + + record_data = self._parse_record_data(record) + if record_data and title_pattern.match(record_data.get('title', '')): + records_to_unshare.add(record_uid) + + def _parse_record_data(self, record: Dict) -> Optional[Dict]: + """Parse record data from JSON with security validation.""" + record_data_json = record.get('data_unencrypted') + if not record_data_json: + return None + + try: + return json.loads(record_data_json) + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.debug("Failed to parse record data: %s", e) + return None + + def _confirm_unshare(self, force: bool, record_count: int) -> bool: + """Confirm unshare operation with user.""" + if force: + return True + + answer = prompt_utils.user_choice( + f'Do you want to remove shares from {record_count} record(s)?', + 'yn', + default='n' + ) + if answer.lower() == 'y': + answer = 'yes' + return answer.lower() == 'yes' + + def _remove_shares_from_records(self, vault, records_to_unshare: List[str]): + """Remove shares from the specified records.""" + record_shares = share_utils.get_record_shares(vault, records_to_unshare, True) + if not record_shares: + return + + remove_share_requests = self._build_remove_share_requests(record_shares) + if not remove_share_requests: + return + + self._execute_share_removal_requests(vault, remove_share_requests) + + def _build_remove_share_requests(self, record_shares: List[Dict]) -> List[record_pb2.SharedRecord]: + """Build remove share requests from record shares.""" + remove_requests = [] + + for record_share in record_shares: + if 'shares' not in record_share: + continue + + shares = record_share['shares'] + if 'user_permissions' not in shares: + continue + + self._process_user_permissions(shares['user_permissions'], record_share['record_uid'], remove_requests) + + return remove_requests + + def _process_user_permissions(self, user_permissions: List[Dict], record_uid: str, remove_requests: List[record_pb2.SharedRecord]) -> None: + """Process user permissions and add to remove requests.""" + for user_permission in user_permissions: + if user_permission.get('owner') is False: + share_request = record_pb2.SharedRecord() + share_request.toUsername = user_permission['username'] + share_request.recordUid = utils.base64_url_decode(record_uid) + remove_requests.append(share_request) + + def _execute_share_removal_requests(self, vault, remove_requests: List[record_pb2.SharedRecord]): + """Execute share removal requests in chunks.""" + while remove_requests: + chunk = remove_requests[:CHUNK_SIZE_LIMIT] + remove_requests = remove_requests[CHUNK_SIZE_LIMIT:] + + self._process_share_removal_chunk(vault, chunk) + + def _process_share_removal_chunk(self, vault, chunk: List[record_pb2.SharedRecord]): + """Process a chunk of share removal requests.""" + update_request = record_pb2.RecordShareUpdateRequest() + update_request.removeSharedRecord.extend(chunk) + + response = vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/records_share_update', + request=update_request, + response_type=record_pb2.RecordShareUpdateResponse + ) + + self._log_share_removal_errors(response) + + def _log_share_removal_errors(self, response: record_pb2.RecordShareUpdateResponse): + """Log any errors from share removal response.""" + for status in response.removeSharedRecordStatus: + if status.status.lower() != 'success': + record_uid = utils.base64_url_encode(status.recordUid) + logger.info('Remove share "%s" from record UID "%s" error: %s', + status.username, record_uid, status.message) diff --git a/keepercli-package/src/keepercli/commands/vault_record.py b/keepercli-package/src/keepercli/commands/vault_record.py index 3e9bea03..ebb767c8 100644 --- a/keepercli-package/src/keepercli/commands/vault_record.py +++ b/keepercli-package/src/keepercli/commands/vault_record.py @@ -1,14 +1,21 @@ import argparse import fnmatch +from functools import reduce import re from typing import Set, Dict, List, Any from . import base -from ..params import KeeperParams -from ..helpers import report_utils, folder_utils from .. import api, prompt_utils -from keepersdk.vault import vault_data, vault_utils, vault_types, record_management, vault_record +from ..params import KeeperParams +from ..helpers import folder_utils, report_utils, share_utils +from keepersdk import utils +from keepersdk.proto import enterprise_pb2 +from keepersdk.vault import record_management, vault_data, vault_types, vault_record, vault_utils + + +logger = api.get_logger() + class RecordListCommand(base.ArgparseCommand): parser = argparse.ArgumentParser(prog='list', description='List records', parents=[base.report_output_parser]) @@ -63,8 +70,259 @@ def execute(self, context: KeeperParams, **kwargs): return report_utils.dump_report_data(table, headers, fmt=fmt, filename=kwargs.get('output'), row_number=True, column_width=None if verbose else 40) else: - api.get_logger().info('No records are found') + logger.info('No records are found') + + +class SharedFolderListCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='list-sf', parents=[base.report_output_parser], + description='Displays shared folders' + ) + SharedFolderListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--verbose', '-v', dest='verbose', action='store_true', + help='verbose output') + parser.add_argument('pattern', nargs='?', metavar='pattern', help='pattern, or UID. Optional') + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise ValueError("Vault is not initialized.") + + pattern = kwargs.get('pattern') + shared_folders = self._find_shared_folders(context, pattern) + + if not shared_folders: + logger.info('No shared folders are found') + return None + + return self._build_shared_folders_report(shared_folders, kwargs) + + def _find_shared_folders(self, context: KeeperParams, pattern: str): + """Find shared folders matching the given pattern.""" + return context.vault.vault_data.find_shared_folders(criteria=pattern) + + def _build_shared_folders_report(self, shared_folders, kwargs): + """Build and format the shared folders report.""" + headers = self._get_shared_folders_headers(kwargs.get('format', 'table')) + table = self._build_shared_folders_table(shared_folders) + + return report_utils.dump_report_data( + table, + headers, + fmt=kwargs.get('format', 'table'), + filename=kwargs.get('output'), + row_number=True, + column_width=None if kwargs.get('verbose') else 40 + ) + + def _get_shared_folders_headers(self, format_type: str): + """Get headers for shared folders report.""" + headers = ['shared_folder_uid', 'name'] + if format_type == 'table': + headers = [report_utils.field_to_title(x) for x in headers] + return headers + def _build_shared_folders_table(self, shared_folders): + """Build table data for shared folders.""" + return [ + [shared_folder.shared_folder_uid, shared_folder.name] + for shared_folder in shared_folders + ] + + +class TeamListCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='list-team', parents=[base.report_output_parser], description='Displays teams' + ) + TeamListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', + help='verbose output (include team membership info)' + ) + parser.add_argument( + '-vv', '--very-verbose', dest='very_verbose', action='store_true', + help='more verbose output (fetches team membership info not in cache)' + ) + parser.add_argument( + '-a', '--all', dest='all', action='store_true', + help='show all teams in your contacts (including those outside your primary organization)' + ) + parser.add_argument( + '--sort', dest='sort', choices=['company', 'team_uid', 'name'], default='company', + help='sort teams by column (default: company)' + ) + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise ValueError("Vault is not initialized.") + + teams = self._get_teams(context, kwargs) + + if not teams: + logger.info('No teams are found') + return None + + return self._build_teams_report(teams, kwargs) + + def _get_teams(self, context: KeeperParams, kwargs): + """Get teams based on filters and options.""" + show_all_teams = kwargs.get('all', False) + show_team_users = kwargs.get('verbose') or kwargs.get('very_verbose', False) + fetch_missing_users = kwargs.get('very_verbose', False) + + # Get teams from share objects + teams = self._get_teams_from_share_objects(context, show_all_teams) + + # Add additional teams if needed + teams = self._add_additional_teams(context, teams) + + # Add team members if requested + if show_team_users: + teams = self.get_team_members(context, teams, fetch_missing_users) + + return teams + + def _get_teams_from_share_objects(self, context: KeeperParams, show_all_teams: bool): + """Get teams from share objects with enterprise filtering.""" + share_objects = share_utils.get_share_objects(vault=context.vault) + teams_data = share_objects.get('teams', {}) + orgs = share_objects.get('enterprises', {}) + + enterprise_id = self._get_current_enterprise_id(context) + is_included = lambda t: show_all_teams or t.get('enterprise_id') == enterprise_id + + teams = [] + for team_uid, team_info in teams_data.items(): + if not is_included(team_info): + continue + teams.append({ + 'team_uid': team_uid, + 'name': team_info.get('name'), + 'enterprise_id': orgs.get(str(team_info.get('enterprise_id'))) + }) + + return teams + + def _get_current_enterprise_id(self, context: KeeperParams): + """Get the current user's enterprise ID.""" + if context.auth.auth_context.license: + return context.auth.auth_context.license.get('enterpriseId') + return None + + def _add_additional_teams(self, context: KeeperParams, teams): + """Add additional teams if the current list is large enough.""" + if len(teams) >= 500: + team_uids = {team['team_uid'] for team in teams} + available_teams = vault_utils.load_available_teams(auth=context.vault.keeper_auth) + + additional_teams = [ + { + 'team_uid': team.team_uid, + 'name': team.name, + 'enterprise_id': self._get_current_enterprise_id(context) + } + for team in available_teams + if team.team_uid not in team_uids + ] + teams.extend(additional_teams) + + return teams + + def _build_teams_report(self, teams, kwargs): + """Build and format the teams report.""" + headers = self._get_teams_headers(kwargs) + table = self._build_teams_table(teams, kwargs) + table = self._sort_teams_table(table, kwargs.get('sort', 'company')) + + return report_utils.dump_report_data( + table, + headers, + fmt=kwargs.get('format', 'table'), + filename=kwargs.get('output'), + row_number=True + ) + + def _get_teams_headers(self, kwargs): + """Get headers for teams report.""" + show_team_users = kwargs.get('verbose') or kwargs.get('very_verbose', False) + fmt = kwargs.get('format', 'table') + + headers = ['company', 'team_uid', 'name'] + if show_team_users: + headers.append('member') + + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + + return headers + + def _build_teams_table(self, teams, kwargs): + """Build table data for teams.""" + show_team_users = kwargs.get('verbose') or kwargs.get('very_verbose', False) + + table = [] + for team in teams: + row = [team.get('enterprise_id'), team.get('team_uid'), team.get('name')] + if show_team_users: + row.append(team.get('members')) + table.append(row) + + return table + + def _sort_teams_table(self, table, sort_column): + """Sort teams table by the specified column.""" + if sort_column == 'company': + table.sort(key=lambda x: (x[0] or '').lower()) + elif sort_column == 'team_uid': + table.sort(key=lambda x: x[1].lower()) + elif sort_column == 'name': + table.sort(key=lambda x: x[2].lower()) + + return table + + @classmethod + def get_team_members(self, context: KeeperParams, teams: List[Dict[str, Any]], allow_fetch: bool) -> List[Dict[str, Any]]: + if not context.enterprise_data: + return teams + + def get_enterprise_teams(): + if not context.enterprise_data: + return {} + users = {x.enterprise_user_id: x.username for x in context.enterprise_data.users.get_all_entities()} + return reduce( + lambda a, b: {**a, b.team_uid: [*a.get(b.team_uid, []), users.get(b.enterprise_user_id)]}, + context.enterprise_data.team_users.get_all_links(), + dict() + ) + + def fetch_members(team_uid: str) -> List[str]: + if not allow_fetch: + return [] + rq = enterprise_pb2.GetTeamMemberRequest() + rq.teamUid = utils.base64_url_decode(team_uid) + rs = context.vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/get_team_members', + request=rq, + response_type=enterprise_pb2.GetTeamMemberResponse + ) + return [x.email for x in rs.enterpriseUser] + + enterprise_teams = get_enterprise_teams() + for t in teams: + t['members'] = enterprise_teams.get(t.get('team_uid')) if enterprise_teams.get(t.get('team_uid')) else fetch_members(t.get('team_uid')) + + return teams class ShortcutCommand(base.GroupCommand): def __init__(self): diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 8b897275..90b798f8 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -26,7 +26,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, - record_type, secrets_manager, share_management, password_report) + record_type, secrets_manager, share_management, password_report, trash, record_file_report) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -37,6 +37,8 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('rndir', vault_folder.FolderRenameCommand(), base.CommandScope.Vault) commands.register_command('mv', vault_folder.FolderMoveCommand(), base.CommandScope.Vault) commands.register_command('list', vault_record.RecordListCommand(), base.CommandScope.Vault, 'l') + commands.register_command('list-sf', vault_record.SharedFolderListCommand(), base.CommandScope.Vault, 'lsf') + commands.register_command('list-team', vault_record.TeamListCommand(), base.CommandScope.Vault, 'lt') commands.register_command('shortcut', vault_record.ShortcutCommand(), base.CommandScope.Vault) commands.register_command('search', record_edit.RecordSearchCommand(), base.CommandScope.Vault, 's') commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') @@ -46,6 +48,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('delete-attachment', record_edit.RecordDeleteAttachmentCommand(), base.CommandScope.Vault) commands.register_command('download-attachment', record_edit.RecordDownloadAttachmentCommand(), base.CommandScope.Vault, 'da') commands.register_command('upload-attachment', record_edit.RecordUploadAttachmentCommand(), base.CommandScope.Vault, 'ua') + commands.register_command('file-report', record_file_report.RecordFileReportCommand(), base.CommandScope.Vault) commands.register_command('import', importer_commands.ImportCommand(), base.CommandScope.Vault) commands.register_command('export', importer_commands.ExportCommand(), base.CommandScope.Vault) commands.register_command('breachwatch', breachwatch.BreachWatchCommand(), base.CommandScope.Vault, 'bw') @@ -64,6 +67,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('share-list', share_management.OneTimeShareListCommand(), base.CommandScope.Vault) commands.register_command('share-create', share_management.OneTimeShareCreateCommand(), base.CommandScope.Vault) commands.register_command('share-remove', share_management.OneTimeShareRemoveCommand(), base.CommandScope.Vault) + commands.register_command('trash', trash.TrashCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/src/keepersdk/enterprise/batch_management.py b/keepersdk-package/src/keepersdk/enterprise/batch_management.py index 2d346a93..dd9fcd28 100644 --- a/keepersdk-package/src/keepersdk/enterprise/batch_management.py +++ b/keepersdk-package/src/keepersdk/enterprise/batch_management.py @@ -582,7 +582,7 @@ def _to_team_requests(self) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]] } if action == EntityAction.Add or action == EntityAction.Update: if isinstance(team.team_uid, str): - rq['node_id'] = team.team_uid + rq['node_id'] = team.node_id existing_team = enterprise_data.teams.get_entity(team.team_uid) if action == EntityAction.Update else None if not team.name and existing_team: diff --git a/keepersdk-package/src/keepersdk/vault/attachment.py b/keepersdk-package/src/keepersdk/vault/attachment.py index 0d56fa5d..68f82045 100644 --- a/keepersdk-package/src/keepersdk/vault/attachment.py +++ b/keepersdk-package/src/keepersdk/vault/attachment.py @@ -13,7 +13,6 @@ from .vault_extensions import resolve_record_access_path from .vault_record import FileRecord, PasswordRecord, TypedRecord, AttachmentFile, AttachmentFileThumb from .. import utils, crypto -from ..authentication import endpoint from ..proto import record_pb2 diff --git a/keepersdk-package/src/keepersdk/vault/record_type_management.py b/keepersdk-package/src/keepersdk/vault/record_type_management.py index 3e103a72..e83bb09a 100644 --- a/keepersdk-package/src/keepersdk/vault/record_type_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_type_management.py @@ -4,9 +4,6 @@ from . import vault_online, record_types from ..proto import record_pb2 -from ..utils import get_logger - -logger = get_logger() def create_custom_record_type(vault: vault_online.VaultOnline, title: str, fields: List[Dict[str, str]], description: str, categories: List[str] = None): is_enterprise_admin = vault.keeper_auth.auth_context.is_enterprise_admin diff --git a/keepersdk-package/src/keepersdk/vault/trash_management.py b/keepersdk-package/src/keepersdk/vault/trash_management.py new file mode 100644 index 00000000..b3033bcd --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/trash_management.py @@ -0,0 +1,818 @@ +import fnmatch +import json +import re +from .. import utils, crypto +from ..proto import folder_pb2, record_pb2 +from ..vault import vault_online +from typing import Callable, Dict, Any, Optional, Tuple, Set, List + +logger = utils.get_logger() + +# Constants +BATCH_SIZE_LIMIT = 1000 +MIN_RECORDS_FOR_BATCH = 100 +PASSWORD_FIELD_TYPE = 'password' +AES_V2_KEY_LENGTH = 60 +RECORD_VERSION_THRESHOLD = 3 + + +class TrashManagement: + """Manages deleted records, orphaned records, and shared folders in the trash. + + This class provides functionality to: + - Load and cache deleted records from various sources + - Decrypt and process shared folder data + - Manage restoration of deleted items + - Handle both regular and orphaned records + + Attributes: + deleted_record_cache: Cache for regular deleted records + orphaned_record_cache: Cache for orphaned records (non-access) + deleted_shared_folder_cache: Cache for deleted shared folders and their records + """ + + deleted_record_cache: Dict[str, Any] = {} + orphaned_record_cache: Dict[str, Any] = {} + deleted_shared_folder_cache: Dict[str, Any] = {} + + @staticmethod + def _ensure_deleted_records_loaded(vault: vault_online.VaultOnline) -> None: + """Load and cache all deleted records, orphaned records, and shared folders. + + This method orchestrates the loading of all trash data from different sources: + - Deleted shared folders and their records from REST API + - Regular deleted records from command API + - Orphaned records from command API + + Args: + vault: The vault instance to load deleted records from + """ + # Load deleted shared folders and records + folder_response = TrashManagement._fetch_deleted_shared_folders_and_records(vault) + if not folder_response: + return + + users = TrashManagement._extract_users(folder_response) + folder_keys = TrashManagement._build_folder_keys(vault) + + # Process shared folders + folders = TrashManagement._process_shared_folders(folder_response, vault, folder_keys) + + # Process shared folder records + record_keys = TrashManagement._process_shared_folder_records( + folder_response, folder_keys + ) + + # Process deleted record data + records = TrashManagement._process_deleted_record_data( + folder_response, record_keys, users + ) + + # Update shared folder cache + TrashManagement._update_shared_folder_cache(folders, records) + + # Load and process deleted records from command + TrashManagement._load_deleted_records_from_command(vault) + + @staticmethod + def _fetch_deleted_shared_folders_and_records(vault: vault_online.VaultOnline) -> Optional[folder_pb2.GetDeletedSharedFoldersAndRecordsResponse]: + """Fetch deleted shared folders and records from the server.""" + return vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/get_deleted_shared_folders_and_records', + request=None, + response_type=folder_pb2.GetDeletedSharedFoldersAndRecordsResponse + ) + + @staticmethod + def _extract_users(folder_response: folder_pb2.GetDeletedSharedFoldersAndRecordsResponse) -> Dict[str, str]: + """Extract user mapping from the folder response.""" + return { + utils.base64_url_encode(x.accountUid): x.username + for x in folder_response.usernames + } + + @staticmethod + def _build_folder_keys(vault: vault_online.VaultOnline) -> Dict[str, Tuple[bytes, str]]: + """Build initial folder keys from existing shared folders.""" + folder_keys = {} + for shared_folder_uid, sf in vault.vault_data._shared_folders.items(): + if sf.shared_folder_key: + folder_keys[shared_folder_uid] = (sf.shared_folder_key, shared_folder_uid) + return folder_keys + + @staticmethod + def _decrypt_folder_key(sf: Any, vault: vault_online.VaultOnline, folder_keys: Dict[str, Tuple[bytes, str]]) -> Optional[bytes]: + """Decrypt folder key based on encryption type.""" + try: + return TrashManagement._decrypt_folder_key_by_type(sf, vault, folder_keys) + except Exception as e: + raise ValueError(f'Folder key decryption failed: {e}') + + @staticmethod + def _decrypt_folder_key_by_type(sf: Any, vault: vault_online.VaultOnline, folder_keys: Dict[str, Tuple[bytes, str]]) -> Optional[bytes]: + """Decrypt folder key based on specific encryption type.""" + key_type = sf.folderKeyType + encrypted_key = sf.sharedFolderKey + auth_context = vault.keeper_auth.auth_context + + # Direct key decryption + if key_type == record_pb2.ENCRYPTED_BY_DATA_KEY: + return crypto.decrypt_aes_v1(encrypted_key, auth_context.data_key) + elif key_type == record_pb2.ENCRYPTED_BY_PUBLIC_KEY: + return crypto.decrypt_rsa(encrypted_key, auth_context.rsa_private_key) + elif key_type == record_pb2.ENCRYPTED_BY_DATA_KEY_GCM: + return crypto.decrypt_aes_v2(encrypted_key, auth_context.data_key) + elif key_type == record_pb2.ENCRYPTED_BY_PUBLIC_KEY_ECC: + return crypto.decrypt_ec(encrypted_key, auth_context.ec_private_key) + + # Root key decryption + elif key_type in (record_pb2.ENCRYPTED_BY_ROOT_KEY_CBC, record_pb2.ENCRYPTED_BY_ROOT_KEY_GCM): + return TrashManagement._decrypt_with_root_key(sf, folder_keys) + + return None + + @staticmethod + def _decrypt_with_root_key(sf: Any, folder_keys: Dict[str, Tuple[bytes, str]]) -> Optional[bytes]: + """Decrypt folder key using root key.""" + shared_folder_uid = utils.base64_url_encode(sf.sharedFolderUid) + if shared_folder_uid not in folder_keys: + return None + + shared_folder_key, _ = folder_keys[shared_folder_uid] + + if sf.folderKeyType == record_pb2.ENCRYPTED_BY_ROOT_KEY_CBC: + return crypto.decrypt_aes_v1(sf.sharedFolderKey, shared_folder_key) + elif sf.folderKeyType == record_pb2.ENCRYPTED_BY_ROOT_KEY_GCM: + return crypto.decrypt_aes_v2(sf.sharedFolderKey, shared_folder_key) + + return None + + @staticmethod + def _process_shared_folders(folder_response: folder_pb2.GetDeletedSharedFoldersAndRecordsResponse, + vault: vault_online.VaultOnline, + folder_keys: Dict[str, Tuple[bytes, str]]) -> Dict[str, Dict[str, Any]]: + """Process and decrypt shared folders.""" + folders = {} + + for shared_folder in folder_response.sharedFolders: + folder_data = TrashManagement._process_single_shared_folder(shared_folder, vault, folder_keys) + if folder_data: + folder_uid = folder_data['folder_uid'] + folders[folder_uid] = folder_data + + return folders + + @staticmethod + def _process_single_shared_folder(sf: Any, vault: vault_online.VaultOnline, folder_keys: Dict[str, Tuple[bytes, str]]) -> Optional[Dict[str, Any]]: + """Process a single shared folder.""" + shared_folder_uid = utils.base64_url_encode(sf.sharedFolderUid) + folder_uid = utils.base64_url_encode(sf.folderUid) + + folder_key = TrashManagement._decrypt_folder_key(sf, vault, folder_keys) + if folder_key is None: + return None + + try: + folder_keys[folder_uid] = (folder_key, shared_folder_uid) + decrypted_data = crypto.decrypt_aes_v1(sf.data, folder_key) + + folder_dict = TrashManagement._create_folder_dict(sf, shared_folder_uid, folder_uid, folder_key, decrypted_data) + return folder_dict + + except Exception as e: + raise ValueError(f'Shared folder data decryption failed: {e}') + + @staticmethod + def _create_folder_dict(sf: Any, shared_folder_uid: str, folder_uid: str, folder_key: bytes, decrypted_data: bytes) -> Dict[str, Any]: + """Create folder dictionary with all necessary data.""" + folder_dict = { + 'shared_folder_uid': shared_folder_uid, + 'folder_uid': folder_uid, + 'data': utils.base64_url_encode(sf.data), + 'data_unencrypted': decrypted_data, + 'folder_key_unencrypted': folder_key, + 'date_deleted': sf.dateDeleted, + } + + if len(sf.parentUid) > 0: + folder_dict['parent_uid'] = utils.base64_url_encode(sf.parentUid) + + return folder_dict + + @staticmethod + def _process_shared_folder_records(folder_response: folder_pb2.GetDeletedSharedFoldersAndRecordsResponse, + folder_keys: Dict[str, Tuple[bytes, str]]) -> Dict[str, Tuple[bytes, str, int]]: + """Process and decrypt shared folder record keys.""" + record_keys = {} + + for record_key_data in folder_response.sharedFolderRecords: + record_key_info = TrashManagement._process_single_shared_folder_record(record_key_data, folder_keys) + if record_key_info: + record_uid, key_data = record_key_info + record_keys[record_uid] = key_data + + return record_keys + + @staticmethod + def _process_single_shared_folder_record(rk: Any, folder_keys: Dict[str, Tuple[bytes, str]]) -> Optional[Tuple[str, Tuple[bytes, str, int]]]: + """Process a single shared folder record key.""" + folder_uid = utils.base64_url_encode(rk.folderUid) + if folder_uid not in folder_keys: + return None + + _, shared_folder_uid = folder_keys[folder_uid] + if shared_folder_uid not in folder_keys: + return None + + folder_key, _ = folder_keys[shared_folder_uid] + record_uid = utils.base64_url_encode(rk.recordUid) + + try: + record_key = TrashManagement._decrypt_shared_record_key(rk.sharedRecordKey, folder_key) + return record_uid, (record_key, folder_uid, rk.dateDeleted) + + except Exception as e: + raise ValueError(f'Record "{record_uid}" key decryption failed: {e}') + + @staticmethod + def _decrypt_shared_record_key(encrypted_key: bytes, folder_key: bytes) -> bytes: + """Decrypt shared record key based on key length.""" + + if len(encrypted_key) == AES_V2_KEY_LENGTH: + return crypto.decrypt_aes_v2(encrypted_key, folder_key) + else: + return crypto.decrypt_aes_v1(encrypted_key, folder_key) + + @staticmethod + def _process_deleted_record_data(folder_response: folder_pb2.GetDeletedSharedFoldersAndRecordsResponse, + record_keys: Dict[str, Tuple[bytes, str, int]], + users: Dict[str, str]) -> Dict[str, Dict[str, Any]]: + """Process and decrypt deleted record data.""" + records = {} + + for record_data in folder_response.deletedRecordData: + record_info = TrashManagement._process_single_deleted_record(record_data, record_keys, users) + if record_info: + record_uid, record_dict = record_info + records[record_uid] = record_dict + + return records + + @staticmethod + def _process_single_deleted_record(r: Any, record_keys: Dict[str, Tuple[bytes, str, int]], users: Dict[str, str]) -> Optional[Tuple[str, Dict[str, Any]]]: + """Process a single deleted record.""" + record_uid = utils.base64_url_encode(r.recordUid) + if record_uid not in record_keys: + return None + + record_key, folder_uid, time_deleted = record_keys[record_uid] + + try: + decrypted_data = TrashManagement._decrypt_record_data_by_version(r.data, r.version, record_key) + record_dict = TrashManagement._create_record_dict(r, record_uid, folder_uid, time_deleted, record_key, decrypted_data, users) + return record_uid, record_dict + + except Exception as e: + raise ValueError(f'Record "{record_uid}" data decryption failed: {e}') + + @staticmethod + def _decrypt_record_data_by_version(encrypted_data: bytes, version: int, record_key: bytes) -> bytes: + """Decrypt record data based on version.""" + if version < RECORD_VERSION_THRESHOLD: + return crypto.decrypt_aes_v1(encrypted_data, record_key) + else: + return crypto.decrypt_aes_v2(encrypted_data, record_key) + + @staticmethod + def _create_record_dict(r: Any, record_uid: str, folder_uid: str, time_deleted: int, record_key: bytes, decrypted_data: bytes, users: Dict[str, str]) -> Dict[str, Any]: + """Create record dictionary with all necessary data.""" + return { + 'record_uid': record_uid, + 'folder_uid': folder_uid, + 'revision': r.revision, + 'version': r.version, + 'owner': users.get(utils.base64_url_encode(r.ownerUid)), + 'client_modified_time': r.clientModifiedTime, + 'date_deleted': time_deleted, + 'data': utils.base64_url_encode(r.data), + 'data_unencrypted': decrypted_data, + 'record_key_unencrypted': record_key, + } + + @staticmethod + def _update_shared_folder_cache(folders: Dict[str, Dict[str, Any]], + records: Dict[str, Dict[str, Any]]) -> None: + """Update the shared folder cache with processed data.""" + cache = TrashManagement.deleted_shared_folder_cache + cache.clear() + + if folders: + cache['folders'] = folders + if records: + cache['records'] = records + + @staticmethod + def _decrypt_record_key(record: Dict[str, Any], vault: vault_online.VaultOnline) -> Optional[bytes]: + """Decrypt record key based on key type.""" + try: + key_type = record['record_key_type'] + record_key = utils.base64_url_decode(record['record_key']) + + if key_type == 1: + return crypto.decrypt_aes_v1(record_key, vault.keeper_auth.auth_context.data_key) + elif key_type == 2: + return crypto.decrypt_rsa(record_key, vault.keeper_auth.auth_context.rsa_private_key) + elif key_type == 3: + return crypto.decrypt_aes_v2(record_key, vault.keeper_auth.auth_context.data_key) + elif key_type == 4: + return crypto.decrypt_ec(record_key, vault.keeper_auth.auth_context.ec_private_key) + else: + raise ValueError(f'Unknown record key type {key_type} for record {record["record_uid"]}') + + except Exception as e: + raise ValueError(f'Record key decryption failed for {record["record_uid"]}: {e}') + + @staticmethod + def _decrypt_record_data(record: Dict[str, Any], record_key: bytes) -> Optional[bytes]: + """Decrypt record data using the provided key.""" + try: + data = utils.base64_url_decode(record['data']) + version = record['version'] + + if version >= 3: + return crypto.decrypt_aes_v2(data, record_key) + else: + return crypto.decrypt_aes_v1(data, record_key) + + except Exception as e: + raise ValueError(f'Record data decryption failed for {record["record_uid"]}: {e}') + + @staticmethod + def _load_deleted_records_from_command(vault: vault_online.VaultOnline) -> None: + """Load deleted records using the get_deleted_records command.""" + request = { + 'command': 'get_deleted_records', + 'client_time': utils.current_milli_time() + } + + response = vault.keeper_auth.execute_auth_command(request) + + # Process both regular and orphaned records + TrashManagement._process_deleted_records_response(response, 'records', TrashManagement.deleted_record_cache, vault) + TrashManagement._process_deleted_records_response(response, 'non_access_records', TrashManagement.orphaned_record_cache, vault) + + @staticmethod + def _process_deleted_records_response(response: Dict[str, Any], record_type: str, cache: Dict[str, Any], vault: vault_online.VaultOnline) -> None: + """Process deleted records response for a specific record type.""" + if record_type not in response: + return + + deleted_uids = set() + + for record in response[record_type]: + record_uid = record['record_uid'] + deleted_uids.add(record_uid) + + if record_uid in cache: + continue + + if TrashManagement._process_single_deleted_record_from_command(record, vault): + cache[record_uid] = record + + # Remove records that are no longer in the deleted list + TrashManagement._cleanup_removed_records(cache, deleted_uids) + + @staticmethod + def _process_single_deleted_record_from_command(record: Dict[str, Any], vault: vault_online.VaultOnline) -> bool: + """Process a single deleted record from command response.""" + record_key = TrashManagement._decrypt_record_key(record, vault) + if record_key is None: + return False + + record['record_key_unencrypted'] = record_key + + decrypted_data = TrashManagement._decrypt_record_data(record, record_key) + if decrypted_data is None: + return False + + record['data_unencrypted'] = decrypted_data + return True + + @staticmethod + def _cleanup_removed_records(cache: Dict[str, Any], current_uids: Set[str]) -> None: + """Remove records from cache that are no longer in the deleted list.""" + for record_uid in list(cache.keys()): + if record_uid not in current_uids: + del cache[record_uid] + + @staticmethod + def get_deleted_records() -> Dict[str, Any]: + """Get all deleted records from cache.""" + return TrashManagement.deleted_record_cache + + @staticmethod + def get_orphaned_records() -> Dict[str, Any]: + """Get all orphaned records from cache.""" + return TrashManagement.orphaned_record_cache + + @staticmethod + def get_shared_folders() -> Dict[str, Any]: + """Get all deleted shared folders from cache.""" + return TrashManagement.deleted_shared_folder_cache + + +def get_trash_record(vault: vault_online.VaultOnline, record_uid: str) -> Tuple[Optional[Dict[str, Any]], bool]: + TrashManagement._ensure_deleted_records_loaded(vault) + deleted_records = TrashManagement.get_deleted_records() + orphaned_records = TrashManagement.get_orphaned_records() + if len(deleted_records) == 0 and len(orphaned_records) == 0: + logger.info('Trash is empty') + return None, False + + is_shared = False + record = deleted_records.get(record_uid) + if not record: + record = orphaned_records.get(record_uid) + is_shared = True + + if not record: + raise ValueError(f'{record_uid} is not a valid deleted record UID') + + return record, is_shared + + +def restore_trash_records(vault: vault_online.VaultOnline, records: List[str], confirm: Optional[Callable[[str], bool]] = None) -> None: + """Restore deleted records from trash. + + Args: + vault: The vault instance + records: List of record UIDs or patterns to restore + confirm: Optional confirmation function + """ + # Load all trash data + trash_data = _load_trash_data(vault) + if _is_trash_empty(trash_data): + logger.info('Trash is empty') + return + + # Identify records and folders to restore + restore_plan = _create_restore_plan(records, trash_data) + if _is_restore_plan_empty(restore_plan): + logger.info('There are no records to restore') + return + + # Confirm restoration if needed + if confirm and not _confirm_restoration(restore_plan, confirm): + return + + # Execute restoration + _execute_record_restoration(vault, restore_plan, trash_data) + _execute_shared_folder_restoration(vault, restore_plan) + _post_restore_processing(vault, restore_plan, trash_data) + + +def _load_trash_data(vault: vault_online.VaultOnline) -> Dict[str, Any]: + """Load all trash data from various sources.""" + TrashManagement._ensure_deleted_records_loaded(vault) + shared_folders = TrashManagement.get_shared_folders() + + return { + 'deleted_records': TrashManagement.get_deleted_records(), + 'orphaned_records': TrashManagement.get_orphaned_records(), + 'deleted_shared_records': shared_folders.get('records', {}), + 'deleted_shared_folders': shared_folders.get('folders', {}) + } + + +def _is_trash_empty(trash_data: Dict[str, Any]) -> bool: + """Check if trash is empty.""" + return (len(trash_data['deleted_records']) == 0 and + len(trash_data['orphaned_records']) == 0 and + len(trash_data['deleted_shared_records']) == 0 and + len(trash_data['deleted_shared_folders']) == 0) + + +def _create_restore_plan(records: List[str], trash_data: Dict[str, Any]) -> Dict[str, Any]: + """Create a plan for what to restore based on the input records.""" + records_to_restore: Set[str] = set() + folders_to_restore: Set[str] = set() + folder_records_to_restore: Dict[str, List[str]] = {} + + for record_id in records: + _process_single_record_for_restore( + record_id, trash_data, records_to_restore, + folders_to_restore, folder_records_to_restore + ) + + # Remove folder records if the entire folder is being restored + for folder_uid in folders_to_restore: + folder_records_to_restore.pop(folder_uid, None) + + return { + 'records_to_restore': records_to_restore, + 'folders_to_restore': folders_to_restore, + 'folder_records_to_restore': folder_records_to_restore + } + + +def _process_single_record_for_restore( + record_id: str, + trash_data: Dict[str, Any], + records_to_restore: Set[str], + folders_to_restore: Set[str], + folder_records_to_restore: Dict[str, List[str]] +) -> None: + """Process a single record ID to determine what should be restored.""" + deleted_records = trash_data['deleted_records'] + orphaned_records = trash_data['orphaned_records'] + deleted_shared_records = trash_data['deleted_shared_records'] + deleted_shared_folders = trash_data['deleted_shared_folders'] + + # Direct UID matches + if record_id in deleted_records or record_id in orphaned_records: + records_to_restore.add(record_id) + elif record_id in deleted_shared_records: + _add_shared_record_to_restore(record_id, deleted_shared_records, folder_records_to_restore) + elif record_id in deleted_shared_folders: + folders_to_restore.add(record_id) + else: + # Pattern matching + _process_pattern_matching( + record_id, trash_data, records_to_restore, + folders_to_restore, folder_records_to_restore + ) + + +def _add_shared_record_to_restore( + record_id: str, + deleted_shared_records: Dict[str, Any], + folder_records_to_restore: Dict[str, List[str]] +) -> None: + """Add a shared record to the restore plan.""" + shared_record = deleted_shared_records.get(record_id) + folder_uid = shared_record.get('folder_uid') + record_uid = shared_record.get('record_uid') + + if folder_uid and record_uid: + if folder_uid not in folder_records_to_restore: + folder_records_to_restore[folder_uid] = [] + folder_records_to_restore[folder_uid].append(record_uid) + + +def _process_pattern_matching( + pattern: str, + trash_data: Dict[str, Any], + records_to_restore: Set[str], + folders_to_restore: Set[str], + folder_records_to_restore: Dict[str, List[str]] +) -> None: + """Process pattern matching for record titles.""" + title_pattern = re.compile(fnmatch.translate(pattern), re.IGNORECASE) + + # Match regular deleted records + _match_records_by_title(trash_data['deleted_records'], title_pattern, records_to_restore) + _match_records_by_title(trash_data['orphaned_records'], title_pattern, records_to_restore) + + # Match shared records + _match_shared_records_by_title(trash_data['deleted_shared_records'], title_pattern, folder_records_to_restore) + + # Match shared folders + _match_folders_by_title(trash_data['deleted_shared_folders'], title_pattern, folders_to_restore) + + +def _match_records_by_title(records: Dict[str, Any], pattern: re.Pattern, records_to_restore: Set[str]) -> None: + """Match records by title pattern.""" + for record_uid, record in records.items(): + if record_uid in records_to_restore: + continue + if _record_title_matches(record, pattern): + records_to_restore.add(record_uid) + + +def _match_shared_records_by_title( + shared_records: Dict[str, Any], + pattern: re.Pattern, + folder_records_to_restore: Dict[str, List[str]] +) -> None: + """Match shared records by title pattern.""" + for record_uid, shared_record in shared_records.items(): + if record_uid in folder_records_to_restore: + continue + if _record_title_matches(shared_record, pattern): + folder_uid = shared_record.get('folder_uid') + if folder_uid: + if folder_uid not in folder_records_to_restore: + folder_records_to_restore[folder_uid] = [] + folder_records_to_restore[folder_uid].append(record_uid) + + +def _match_folders_by_title(folders: Dict[str, Any], pattern: re.Pattern, folders_to_restore: Set[str]) -> None: + """Match folders by name pattern.""" + for folder_uid, folder in folders.items(): + if folder_uid in folders_to_restore: + continue + if _folder_name_matches(folder, pattern, folder_uid): + folders_to_restore.add(folder_uid) + + +def _record_title_matches(record: Dict[str, Any], pattern: re.Pattern) -> bool: + """Check if a record's title matches the pattern.""" + try: + record_data_json = record.get('data_unencrypted') + if not record_data_json: + return False + record_data = json.loads(record_data_json) + title = record_data.get('title', '') + return pattern.match(title) is not None + except (json.JSONDecodeError, KeyError): + return False + + +def _folder_name_matches(folder: Dict[str, Any], pattern: re.Pattern, folder_uid: str) -> bool: + """Check if a folder's name matches the pattern.""" + try: + data_json = folder.get('data_unencrypted') + if not data_json: + return False + data = json.loads(data_json) + folder_name = data.get('name') or folder_uid + return pattern.match(folder_name) is not None + except (json.JSONDecodeError, KeyError): + return False + + +def _is_restore_plan_empty(restore_plan: Dict[str, Any]) -> bool: + """Check if the restore plan is empty.""" + record_count = len(restore_plan['records_to_restore']) + for folder_records in restore_plan['folder_records_to_restore'].values(): + record_count += len(folder_records) + folder_count = len(restore_plan['folders_to_restore']) + + return record_count == 0 and folder_count == 0 + + +def _confirm_restoration(restore_plan: Dict[str, Any], confirm_func: Callable[[str], bool]) -> bool: + """Confirm restoration with the user.""" + record_count = len(restore_plan['records_to_restore']) + for folder_records in restore_plan['folder_records_to_restore'].values(): + record_count += len(folder_records) + folder_count = len(restore_plan['folders_to_restore']) + + to_restore = [] + if record_count > 0: + to_restore.append(f'{record_count} record(s)') + if folder_count > 0: + to_restore.append(f'{folder_count} folder(s)') + + question = f'Do you want to restore {" and ".join(to_restore)}?' + answer = confirm_func(question) + + if answer.lower() == 'y': + answer = 'yes' + return answer.lower() == 'yes' + + +def _execute_record_restoration(vault: vault_online.VaultOnline, restore_plan: Dict[str, Any], trash_data: Dict[str, Any]) -> None: + """Execute restoration of regular records.""" + records_to_restore = restore_plan['records_to_restore'] + if not records_to_restore: + return + + deleted_records = trash_data['deleted_records'] + orphaned_records = trash_data['orphaned_records'] + + batch = [] + for record_uid in records_to_restore: + record = deleted_records.get(record_uid) or orphaned_records.get(record_uid) + request = { + 'command': 'undelete_record', + 'record_uid': record_uid, + } + if 'revision' in record: + request['revision'] = record['revision'] + batch.append(request) + + vault.keeper_auth.execute_batch(batch) + + +def _execute_shared_folder_restoration(vault: vault_online.VaultOnline, restore_plan: Dict[str, Any]) -> None: + """Execute restoration of shared folders and their records.""" + folders_to_restore = restore_plan['folders_to_restore'] + folder_records_to_restore = restore_plan['folder_records_to_restore'] + + if not folders_to_restore and not folder_records_to_restore: + return + + shared_folder_requests = _create_shared_folder_requests(folders_to_restore) + shared_folder_record_requests = _create_shared_folder_record_requests(folder_records_to_restore) + + _process_shared_folder_batches(vault, shared_folder_requests, shared_folder_record_requests) + + +def _create_shared_folder_requests(folders_to_restore: Set[str]) -> List[folder_pb2.RestoreSharedObject]: + """Create requests for restoring shared folders.""" + requests = [] + for folder_uid in folders_to_restore: + request = folder_pb2.RestoreSharedObject() + request.folderUid = utils.base64_url_decode(folder_uid) + requests.append(request) + return requests + + +def _create_shared_folder_record_requests(folder_records_to_restore: Dict[str, List[str]]) -> List[folder_pb2.RestoreSharedObject]: + """Create requests for restoring shared folder records.""" + requests = [] + for folder_uid, record_uids in folder_records_to_restore.items(): + request = folder_pb2.RestoreSharedObject() + request.folderUid = utils.base64_url_decode(folder_uid) + request.recordUids.extend((utils.base64_url_decode(uid) for uid in record_uids)) + requests.append(request) + return requests + + +def _process_shared_folder_batches( + vault: vault_online.VaultOnline, + shared_folder_requests: List[folder_pb2.RestoreSharedObject], + shared_folder_record_requests: List[folder_pb2.RestoreSharedObject] +) -> None: + """Process shared folder restoration in batches.""" + while shared_folder_requests or shared_folder_record_requests: + request = folder_pb2.RestoreDeletedSharedFoldersAndRecordsRequest() + remaining_space = BATCH_SIZE_LIMIT + + # Add folder requests + if shared_folder_requests: + chunk_size = min(len(shared_folder_requests), remaining_space) + chunk = shared_folder_requests[:chunk_size] + shared_folder_requests = shared_folder_requests[chunk_size:] + remaining_space -= len(chunk) + request.folders.extend(chunk) + + # Add record requests if there's space + if shared_folder_record_requests and remaining_space > MIN_RECORDS_FOR_BATCH: + chunk_size = min(len(shared_folder_record_requests), remaining_space) + chunk = shared_folder_record_requests[:chunk_size] + shared_folder_record_requests = shared_folder_record_requests[chunk_size:] + request.records.extend(chunk) + + vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/restore_deleted_shared_folders_and_records', + request=request, + response_type=None + ) + + +def _post_restore_processing(vault: vault_online.VaultOnline, restore_plan: Dict[str, Any], trash_data: Dict[str, Any]) -> None: + """Perform post-restoration processing like breach watch scanning.""" + vault.sync_down() + + records_to_restore = restore_plan['records_to_restore'] + if not records_to_restore: + return + + deleted_records = trash_data['deleted_records'] + orphaned_records = trash_data['orphaned_records'] + breach_watch = vault.breach_watch_plugin() + + for record_uid in records_to_restore: + record_key = vault.vault_data.get_record_key(record_uid) + record = deleted_records.get(record_uid) or orphaned_records.get(record_uid) + password = _extract_password_from_record(record) + + breach_watch.scan_and_store_record_status(record_uid, record_key, password) + vault.client_audit_event_plugin().schedule_audit_event('record_restored', record_uid=record_uid) + + vault.sync_down() + + +def _extract_password_from_record(record: Dict[str, Any]) -> Optional[str]: + """Extract password from record data.""" + try: + record_data_json = record.get('data_unencrypted') + if not record_data_json: + return None + + record_data = json.loads(record_data_json) + fields = record_data.get('fields', {}) + + for field in fields: + if field.get('type') == PASSWORD_FIELD_TYPE: + password = field.get('value') + if isinstance(password, list): + password = password[0] + return password + return None + except (json.JSONDecodeError, KeyError): + return None + + +def purge_trash(vault: vault_online.VaultOnline) -> None: + """Permanently delete all records in trash. + + Args: + vault: The vault instance + """ + request = { + 'command': 'purge_deleted_records' + } + vault.keeper_auth.execute_auth_command(request) \ No newline at end of file From 796a02c9a38051517fa0af21eaf9c65d7322d682 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 19 Sep 2025 18:54:01 +0530 Subject: [PATCH 30/44] Team handling bug in get command --- .../src/keepercli/commands/enterprise_team.py | 5 ++++- .../keepercli/commands/enterprise_utils.py | 4 +--- .../src/keepercli/commands/trash.py | 9 +++++--- .../src/keepersdk/vault/trash_management.py | 21 ++++++++++++------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/enterprise_team.py b/keepercli-package/src/keepercli/commands/enterprise_team.py index a56733cc..e9745db9 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_team.py +++ b/keepercli-package/src/keepercli/commands/enterprise_team.py @@ -34,7 +34,10 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: verbose = kwargs.get('verbose') is True enterprise_data = context.enterprise_data - team = enterprise_utils.TeamUtils.resolve_single_team(enterprise_data, kwargs.get('team')) + team_name = kwargs.get('team') + team = enterprise_utils.TeamUtils.resolve_single_team(enterprise_data, team_name) + if team is None: + raise base.CommandError(f'Team name \"{team_name}\" does not exist') node_name = enterprise_utils.NodeUtils.get_node_path(enterprise_data, team.node_id, omit_root=False) team_obj = { 'team_uid': team.team_uid, diff --git a/keepercli-package/src/keepercli/commands/enterprise_utils.py b/keepercli-package/src/keepercli/commands/enterprise_utils.py index aeee9a34..d2ea3f6e 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_utils.py +++ b/keepercli-package/src/keepercli/commands/enterprise_utils.py @@ -398,7 +398,7 @@ def resolve_queued_teams(e_data: enterprise_types.IEnterpriseData, return list(found_teams.values()), missing_teams @staticmethod - def resolve_single_team(e_data: enterprise_types.IEnterpriseData, team_name: Any) -> enterprise_types.Team: + def resolve_single_team(e_data: enterprise_types.IEnterpriseData, team_name: Any) -> Optional[enterprise_types.Team]: team: Optional[enterprise_types.Team] = None if isinstance(team_name, str): team = e_data.teams.get_entity(team_name) @@ -408,8 +408,6 @@ def resolve_single_team(e_data: enterprise_types.IEnterpriseData, team_name: Any raise base.CommandError(f'Team name \"{team_name}\" is not unique. Please use Node UID') elif len(ts) == 1: team = ts[0] - if team is None: - raise base.CommandError(f'Team name \"{team_name}\" does not exist') return team diff --git a/keepercli-package/src/keepercli/commands/trash.py b/keepercli-package/src/keepercli/commands/trash.py index 0698b0eb..d05ac17b 100644 --- a/keepercli-package/src/keepercli/commands/trash.py +++ b/keepercli-package/src/keepercli/commands/trash.py @@ -64,7 +64,10 @@ def execute(self, context: KeeperParams, **kwargs): return pattern = self._normalize_search_pattern(kwargs.get('pattern')) - title_pattern = self._create_title_pattern(pattern) + if not pattern: + title_pattern = None + else: + title_pattern = self._create_title_pattern(pattern) headers = ['Folder UID', 'Record UID', 'Name', 'Record Type', 'Deleted At', 'Status'] record_table = self._build_record_table(deleted_records, orphaned_records, pattern, title_pattern) @@ -624,13 +627,13 @@ def _find_records_to_unshare(self, record_patterns: List[str], orphaned_records: def _add_matching_records(self, pattern: str, orphaned_records: Dict, records_to_unshare: set): """Add records matching the pattern to the unshare set.""" if len(pattern) > STRING_LENGTH_LIMIT: # Prevent ReDoS - logger.warning("Pattern too long, truncated") + logger.warning("Record name too long, truncated") pattern = pattern[:STRING_LENGTH_LIMIT] try: title_pattern = re.compile(fnmatch.translate(pattern), re.IGNORECASE) except re.error as e: - raise base.CommandError("Invalid pattern: %s", e) + raise base.CommandError("Invalid record name: %s", e) for record_uid, record in orphaned_records.items(): if record_uid in records_to_unshare: diff --git a/keepersdk-package/src/keepersdk/vault/trash_management.py b/keepersdk-package/src/keepersdk/vault/trash_management.py index b3033bcd..c41eff9b 100644 --- a/keepersdk-package/src/keepersdk/vault/trash_management.py +++ b/keepersdk-package/src/keepersdk/vault/trash_management.py @@ -106,7 +106,8 @@ def _decrypt_folder_key(sf: Any, vault: vault_online.VaultOnline, folder_keys: D try: return TrashManagement._decrypt_folder_key_by_type(sf, vault, folder_keys) except Exception as e: - raise ValueError(f'Folder key decryption failed: {e}') + logger.debug('Folder key decryption failed: %s', e) + return None @staticmethod def _decrypt_folder_key_by_type(sf: Any, vault: vault_online.VaultOnline, folder_keys: Dict[str, Tuple[bytes, str]]) -> Optional[bytes]: @@ -180,7 +181,8 @@ def _process_single_shared_folder(sf: Any, vault: vault_online.VaultOnline, fold return folder_dict except Exception as e: - raise ValueError(f'Shared folder data decryption failed: {e}') + logger.debug('Shared folder data decryption failed: %s', e) + return None @staticmethod def _create_folder_dict(sf: Any, shared_folder_uid: str, folder_uid: str, folder_key: bytes, decrypted_data: bytes) -> Dict[str, Any]: @@ -232,7 +234,8 @@ def _process_single_shared_folder_record(rk: Any, folder_keys: Dict[str, Tuple[b return record_uid, (record_key, folder_uid, rk.dateDeleted) except Exception as e: - raise ValueError(f'Record "{record_uid}" key decryption failed: {e}') + logger.debug('Record "%s" key decryption failed: %s', record_uid, e) + return None @staticmethod def _decrypt_shared_record_key(encrypted_key: bytes, folder_key: bytes) -> bytes: @@ -273,7 +276,8 @@ def _process_single_deleted_record(r: Any, record_keys: Dict[str, Tuple[bytes, s return record_uid, record_dict except Exception as e: - raise ValueError(f'Record "{record_uid}" data decryption failed: {e}') + logger.debug('Record "%s" data decryption failed: %s', record_uid, e) + return None @staticmethod def _decrypt_record_data_by_version(encrypted_data: bytes, version: int, record_key: bytes) -> bytes: @@ -327,10 +331,12 @@ def _decrypt_record_key(record: Dict[str, Any], vault: vault_online.VaultOnline) elif key_type == 4: return crypto.decrypt_ec(record_key, vault.keeper_auth.auth_context.ec_private_key) else: - raise ValueError(f'Unknown record key type {key_type} for record {record["record_uid"]}') + logger.debug('Unknown record key type %d for record %s', key_type, record['record_uid']) + return None except Exception as e: - raise ValueError(f'Record key decryption failed for {record["record_uid"]}: {e}') + logger.debug('Record key decryption failed for %s: %s', record['record_uid'], e) + return None @staticmethod def _decrypt_record_data(record: Dict[str, Any], record_key: bytes) -> Optional[bytes]: @@ -345,7 +351,8 @@ def _decrypt_record_data(record: Dict[str, Any], record_key: bytes) -> Optional[ return crypto.decrypt_aes_v1(data, record_key) except Exception as e: - raise ValueError(f'Record data decryption failed for {record["record_uid"]}: {e}') + logger.debug('Record data decryption failed for %s: %s', record['record_uid'], e) + return None @staticmethod def _load_deleted_records_from_command(vault: vault_online.VaultOnline) -> None: From 1f35210d5357660e684a6d2832e0fee66df3cb35 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 26 Sep 2025 18:18:06 +0530 Subject: [PATCH 31/44] Clipboard Copy and Record History commands added --- .../keepercli/biometric/platforms/windows.py | 31 - .../commands/record_handling_commands.py | 702 ++++++++++++++++++ .../src/keepercli/register_commands.py | 6 +- 3 files changed, 707 insertions(+), 32 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/record_handling_commands.py diff --git a/keepercli-package/src/keepercli/biometric/platforms/windows.py b/keepercli-package/src/keepercli/biometric/platforms/windows.py index 68ed1e0b..a60498ef 100644 --- a/keepercli-package/src/keepercli/biometric/platforms/windows.py +++ b/keepercli-package/src/keepercli/biometric/platforms/windows.py @@ -166,37 +166,6 @@ async def _check_biometrics(self) -> bool: logging.debug("Failed to check biometrics availability: %s", str(e)) return False - # def _check_biometrics(self) -> bool: - # """Check if biometrics (face/fingerprint) are enrolled""" - # sid = self._get_current_user_sid() - # if not sid: - # return False - - # reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\WinBio\AccountInfo\{}".format(sid) - # try: - # import winreg - # with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: - # value, regtype = winreg.QueryValueEx(key, "EnrolledFactors") - # # 2 = Face, 8 = Fingerprint, 10 = Face and Fingerprint - # return value in [2, 8, 10] - # except (FileNotFoundError, ImportError): - # return False - - # def _check_pin_enrollment(self) -> bool: - # """Check if PIN is enrolled""" - # sid = self._get_current_user_sid() - # if not sid: - # return False - - # reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{{D6886603-9D2F-4EB2-B667-1971041FA96B}}\{}".format(sid) - # try: - # import winreg - # with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: - # value, regtype = winreg.QueryValueEx(key, "LogonCredsAvailable") - # return value == 1 - # except (FileNotFoundError, ImportError): - # return False - def detect_capabilities(self) -> Tuple[bool, str]: """Detect Windows Hello capabilities""" if os.name != 'nt': diff --git a/keepercli-package/src/keepercli/commands/record_handling_commands.py b/keepercli-package/src/keepercli/commands/record_handling_commands.py new file mode 100644 index 00000000..ef217ac3 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/record_handling_commands.py @@ -0,0 +1,702 @@ +import argparse +import datetime +import json +import re +from typing import Optional, List + +from colorama import Fore, Back, Style + +from keepersdk.proto import record_pb2 +from keepersdk.vault import (record_types, vault_record, vault_online) +from keepersdk import crypto, utils + +from . import base +from ..helpers import folder_utils, record_utils, report_utils +from .. import api +from ..params import KeeperParams + + +logger = api.get_logger() +MAX_VERSION_COUNT = 5 +TRUNCATE_LENGTH = 52 + + +class ClipboardCommand(base.ArgparseCommand): + """Command to copy record data to clipboard or output to various destinations.""" + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='clipboard-copy', + description='Retrieve the password for a specific record.' + ) + self.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command line arguments to the parser.""" + parser.add_argument( + '--username', + dest='username', + action='store', + help='match login name (optional)' + ) + parser.add_argument( + '--output', + dest='output', + choices=['clipboard', 'stdout', 'stdouthidden', 'variable'], + default='clipboard', + action='store', + help='password output destination' + ) + parser.add_argument( + '--name', + dest='name', + action='store', + help='Variable name if output is set to variable' + ) + parser.add_argument( + '-cu', '--copy-uid', + dest='copy_uid', + action='store_true', + help='output uid instead of password' + ) + parser.add_argument( + '-l', '--login', + dest='login', + action='store_true', + help='output login name' + ) + parser.add_argument( + '-t', '--totp', + dest='totp', + action='store_true', + help='output totp code' + ) + parser.add_argument( + '--field', + dest='field', + action='store', + help='output custom field' + ) + parser.add_argument( + '-r', '--revision', + dest='revision', + type=int, + action='store', + help='use a specific record revision' + ) + parser.add_argument( + 'record', + nargs='?', + type=str, + action='store', + help='record path or UID' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the clipboard copy command.""" + self._validate_vault(context) + + record_name = kwargs.get('record', '') + if not record_name: + self.get_parser().print_help() + return + + user_pattern = self._create_user_pattern(kwargs.get('username')) + record_uid = self._find_record_uid(context, record_name, user_pattern) + + if not record_uid: + raise base.CommandError('Enter name or uid of existing record') + + record = self._load_record_with_revision(context, record_uid, kwargs.get('revision')) + if not record: + logger.info(f'Record UID {record_uid} cannot be loaded.') + return + + copy_item, text = self._extract_record_data(record, kwargs) + if text: + self._output_data(copy_item, text, kwargs, context, record_uid) + + def _validate_vault(self, context: KeeperParams): + """Validate that vault is initialized.""" + if not context.vault: + raise ValueError('Vault is not initialized. Login to initialize the vault.') + + def _create_user_pattern(self, username: Optional[str]) -> Optional[re.Pattern]: + """Create regex pattern for username matching.""" + if not username: + return None + # Escape special regex characters to prevent ReDoS attacks + escaped_username = re.escape(username) + return re.compile(escaped_username, re.IGNORECASE) + + def _find_record_uid(self, context: KeeperParams, record_name: str, user_pattern: Optional[re.Pattern]) -> Optional[str]: + """Find record UID by name or path.""" + + if record_name in context.vault.vault_data._records: + return record_name + + path_result = folder_utils.try_resolve_path(context, record_name) + if path_result is not None: + folder, record_name = path_result + if folder and record_name: + return self._find_record_in_folder(context, folder, record_name, user_pattern) + + return self._search_records_in_vault(context, record_name, user_pattern) + + def _find_record_in_folder(self, context: KeeperParams, folder, record_name: str, user_pattern: Optional[re.Pattern]) -> Optional[str]: + """Find record in specific folder.""" + for folder_record_uid in folder.records: + record = context.vault.vault_data.load_record(folder_record_uid) + if not isinstance(record, (vault_record.PasswordRecord, vault_record.TypedRecord)): + continue + if record.title.lower() == record_name.lower(): + if self._matches_user_pattern(record, user_pattern): + return folder_record_uid + return None + + def _search_records_in_vault(self, context: KeeperParams, record_name: str, user_pattern: Optional[re.Pattern]) -> Optional[str]: + """Search for records in vault by name.""" + records = [] + for record in context.vault.vault_data.find_records(criteria=record_name): + if isinstance(record, (vault_record.PasswordRecord, vault_record.TypedRecord)): + if self._matches_user_pattern(record, user_pattern): + records.append(record) + + if len(records) == 0: + raise base.CommandError('Enter name or uid of existing record') + elif len(records) > 1: + records = self._filter_exact_matches(records, record_name) + if len(records) > 1: + raise base.CommandError(f'More than one record are found for search criteria: {record_name}') + + if context.vault and 'output' in context.vault.__dict__ and context.vault.output == 'clipboard': + logger.info('Record Title: %s', records[0].title) + return records[0].record_uid + + def _filter_exact_matches(self, records: List, record_name: str) -> List: + """Filter records to exact title matches.""" + try: + # Escape special regex characters to prevent ReDoS attacks + escaped_record_name = re.escape(record_name) + pattern = re.compile(escaped_record_name, re.IGNORECASE).search + exact_title = [x for x in records if pattern(x.title)] + if len(exact_title) == 1: + return exact_title + except Exception: + pass + return records + + def _matches_user_pattern(self, record, user_pattern: Optional[re.Pattern]) -> bool: + """Check if record matches user pattern.""" + if not user_pattern: + return True + + login = self._get_record_login(record) + return bool(login and user_pattern.match(login)) + + def _get_record_login(self, record) -> str: + """Extract login from record.""" + if isinstance(record, vault_record.PasswordRecord): + return record.login + elif isinstance(record, vault_record.TypedRecord): + login_field = record.get_typed_field('login') + if login_field is None: + login_field = record.get_typed_field('email') + if login_field: + return login_field.get_default_value(str) + return '' + + def _load_record_with_revision(self, context: KeeperParams, record_uid: str, revision: Optional[int]): + """Load record with optional revision.""" + if revision is not None: + history = self._load_record_history(context, record_uid) + if not history: + logger.info('Record does not have history of edit') + return None + + length = len(history) + if revision < 0: + revision = length + revision + if revision <= 0 or revision >= length: + logger.info(f'Invalid revision {revision}: valid revisions 1..{length - 1}') + return None + + revision_index = 0 if revision == 0 else length - revision + return context.vault.vault_data.load_record(history[revision_index]) + else: + return context.vault.vault_data.load_record(record_uid) + + def _extract_record_data(self, record, kwargs) -> tuple[str, str]: + """Extract data from record based on command options.""" + if kwargs.get('copy_uid'): + return 'Record UID', record.record_uid + elif kwargs.get('login'): + return 'Login', self._get_record_login(record) + elif kwargs.get('totp'): + return self._extract_totp_data(record) + elif kwargs.get('field'): + return self._extract_field_data(record, kwargs['field']) + else: + return self._extract_password_data(record) + + def _extract_totp_data(self, record) -> tuple[str, str]: + """Extract TOTP data from record.""" + totp_url = None + if isinstance(record, vault_record.PasswordRecord): + totp_url = record.totp + elif isinstance(record, vault_record.TypedRecord): + totp_field = record.get_typed_field('oneTimeCode') + if totp_field is None: + totp_field = record.get_typed_field('otp') + if totp_field: + totp_url = totp_field.get_default_value(str) + + if totp_url: + result = record_utils.get_totp_code(totp_url) + if result: + return 'TOTP Code', result[0] + return 'TOTP Code', '' + + def _extract_field_data(self, record, field_name: str) -> tuple[str, str]: + """Extract custom field data from record.""" + if field_name == 'notes': + notes = record.notes if hasattr(record, 'notes') else '' + return 'Notes', notes + else: + return self._extract_custom_field_data(record, field_name) + + def _extract_custom_field_data(self, record, field_name: str) -> tuple[str, str]: + """Extract custom field data from record.""" + copy_item = f'Custom Field "{field_name}"' + field_name, field_property = self._parse_field_name(field_name) + + if isinstance(record, vault_record.PasswordRecord): + return copy_item, record.custom.get(field_name, '') + elif isinstance(record, vault_record.TypedRecord): + return self._extract_typed_field_data(record, field_name, field_property, copy_item) + + return copy_item, '' + + def _parse_field_name(self, field_name: str) -> tuple[str, str]: + """Parse field name and property.""" + pre, sep, prop = field_name.rpartition(':') + if sep == ':': + return pre, prop + return field_name, '' + + def _extract_typed_field_data(self, record, field_name: str, field_property: str, copy_item: str) -> tuple[str, str]: + """Extract data from typed field.""" + field_type, sep, field_label = field_name.partition('.') + rf = record_types.RecordFields.get(field_type) + ft = record_types.FieldTypes.get(rf.type) if rf else None + + if ft is None: + field_label = field_name + field_type = 'text' + + field = record.get_typed_field(field_type, field_label) + if not field: + return copy_item, '' + + copy_item = f'Field "{field_name}"' + + if ft and field_property and isinstance(ft.value, dict): + f_value = field.get_default_value(dict) + if f_value: + field_property = next( + (x for x in ft.value.keys() if x.lower().startswith(field_property.lower())), + None + ) + if field_property: + return copy_item, f_value.get(field_property, '') + else: + return copy_item, json.dumps(f_value, indent=2) + else: + return copy_item, '\n'.join(field.get_external_value()) + + def _extract_password_data(self, record) -> tuple[str, str]: + """Extract password data from record.""" + if isinstance(record, vault_record.PasswordRecord): + return 'Password', record.password + elif isinstance(record, vault_record.TypedRecord): + password_field = record.get_typed_field('password') + if password_field: + return 'Password', password_field.get_default_value(str) + return 'Password', '' + + def _output_data(self, copy_item: str, text: str, kwargs: dict, context: KeeperParams, record_uid: str): + """Output data to specified destination.""" + output_type = kwargs.get('output', 'clipboard') + + if output_type == 'clipboard': + import pyperclip + pyperclip.copy(text) + logger.info(f'{copy_item} copied to clipboard') + elif output_type == 'stdouthidden': + logger.info(f'{Fore.RED}{Back.RED}{text}{Style.RESET_ALL}') + elif output_type == 'variable': + var_name = kwargs.get('name') + if not var_name: + raise base.CommandError('"name" parameter is required when "output" is set to "variable"') + context.environment_variables[var_name] = text + logger.info(f'{copy_item} is set to variable "{var_name}"') + else: + logger.info(text) + + # Schedule audit event for password copy + if copy_item == 'Password' and text: + context.vault.client_audit_event_plugin().schedule_audit_event('copy_password', record_uid=record_uid) + + def _load_record_history(self, context: KeeperParams, record_uid: str) -> Optional[list]: + """Load record history from server.""" + if not context.vault: + raise ValueError('Vault is not initialized. Login to initialize the vault.') + + return self._load_record_history_static(context.vault, record_uid) + + @staticmethod + def _load_record_history_static(vault: vault_online.VaultOnline, record_uid: str) -> Optional[list]: + """Load record history from server (static method for sharing).""" + current_rec = vault.vault_data._records[record_uid] + record_key = current_rec.record_key + + request = { + 'command': 'get_record_history', + 'record_uid': record_uid, + 'client_time': utils.current_milli_time() + } + + try: + response = vault.keeper_auth.execute_auth_command(request) + except Exception as e: + logger.error('Cannot load record history: %s', e) + return None + + history = response['history'] + history.sort(key=lambda x: x.get('revision', 0), reverse=True) + + for rec in history: + rec['record_key_unencrypted'] = record_key + ClipboardCommand._decrypt_history_record_static(rec, record_key) + + return history + + @staticmethod + def _decrypt_history_record_static(rec: dict, record_key: bytes): + """Decrypt history record data (static method for sharing).""" + if 'data' in rec: + data = utils.base64_url_decode(rec['data']) + version = rec.get('version', 0) + try: + if version <= 2: + rec['data_unencrypted'] = crypto.decrypt_aes_v1(data, record_key) + else: + rec['data_unencrypted'] = crypto.decrypt_aes_v2(data, record_key) + + if 'extra' in rec: + extra = utils.base64_url_decode(rec['extra']) + if version <= 2: + rec['extra_unencrypted'] = crypto.decrypt_aes_v1(extra, record_key) + else: + rec['extra_unencrypted'] = crypto.decrypt_aes_v2(extra, record_key) + except Exception as e: + logger.warning('Cannot decrypt record history revision: %s', e) + rec['data_unencrypted'] = None + rec['extra_unencrypted'] = None + + +class RecordHistoryCommand(base.ArgparseCommand): + """Command to show and manage record modification history.""" + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='record-history', + parents=[base.report_output_parser], + description='Show the history of a record modifications.' + ) + self.add_arguments_to_parser(self.parser) + super(RecordHistoryCommand, self).__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command line arguments to the parser.""" + parser.add_argument( + '-a', '--action', + dest='action', + choices=['list', 'diff', 'view', 'restore'], + action='store', + help="filter by record history type. (default: 'list'). --revision required with 'restore' action." + ) + parser.add_argument( + '-r', '--revision', + dest='revision', + type=int, + action='store', + help='only show the details for a specific revision.' + ) + parser.add_argument( + '-v', '--verbose', + dest='verbose', + action='store_true', + help="verbose output" + ) + parser.add_argument( + 'record', + nargs='?', + type=str, + action='store', + help='record path or UID' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the record history command.""" + self._validate_vault(context) + + vault = context.vault + record_name = kwargs.get('record') + if not record_name: + self.get_parser().print_help() + return + + record_uid = self._find_record_uid(context, record_name) + if not record_uid: + raise base.CommandError('Record not found: Enter name of existing record') + + history = ClipboardCommand._load_record_history_static(vault, record_uid) + if not history: + logger.info('Record does not have history of edit') + return + + action = kwargs.get('action') or 'list' + self._execute_action(action, vault, history, kwargs) + + def _validate_vault(self, context: KeeperParams): + """Validate that vault is initialized.""" + if not context.vault: + raise ValueError('Vault is not initialized. Login to initialize the vault.') + + def _find_record_uid(self, context: KeeperParams, record_name: str) -> Optional[str]: + """Find record UID by name or path.""" + + vault = context.vault + if record_name in vault.vault_data._records: + return record_name + + path_result = folder_utils.try_resolve_path(context, record_name) + if path_result is not None: + folder, record_name = path_result + if folder and record_name: + return self._find_record_in_folder(vault, folder, record_name) + + return None + + def _find_record_in_folder(self, vault: vault_online.VaultOnline, folder, record_name: str) -> Optional[str]: + """Find record in specific folder.""" + for folder_record_uid in folder.records: + record = vault.vault_data.load_record(folder_record_uid) + if record.title.lower() == record_name.lower(): + return folder_record_uid + return None + + def _execute_action(self, action: str, vault: vault_online.VaultOnline, history: list, kwargs: dict): + """Execute the specified history action.""" + if action == 'list': + self._list_history(history, kwargs) + elif action == 'view': + self._view_revision(history, kwargs) + elif action == 'diff': + self._show_diff(history, kwargs) + elif action == 'restore': + self._restore_revision(vault, history, kwargs) + + def _list_history(self, history: list, kwargs: dict): + """List record history revisions.""" + fmt = kwargs.get('format', '') + headers = ['version', 'modified_by', 'time_modified'] + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + + rows = [] + length = len(history) + for i, version in enumerate(history): + dt = None + if 'client_modified_time' in version: + dt = datetime.datetime.fromtimestamp(int(version['client_modified_time'] / 1000.0)) + version_label = f'V.{length-i}' if i > 0 else 'Current' + rows.append([version_label, version.get('user_name', ''), dt]) + + return report_utils.dump_report_data(rows, headers, fmt=fmt, filename=kwargs.get('output')) + + def _view_revision(self, history: list, kwargs: dict): + """View a specific revision.""" + revision = kwargs.get('revision') or 0 + length = len(history) + + if revision < 0 or revision >= length: + raise ValueError(f'Invalid revision {revision}: valid revisions 1..{length - 1}') + + index = 0 if revision == 0 else length - revision + rev = history[index] + record_data_bytes = rev['data_unencrypted'] + record_data = json.loads(record_data_bytes) + + rows = [] + rows.append(['Title', record_data.get('title')]) + rows.append(['Type', record_data.get('type')]) + fields = record_data.get('fields', []) + for field in fields: + label = field.get('label') + if not label or label == '': + label = field.get('type') + value = field.get('value') + if value: + if isinstance(value, list): + value = '\n'.join(value) + rows.append([label, value]) + + modified = datetime.datetime.fromtimestamp(int(rev['client_modified_time'] / 1000.0)) + rows.append(['Modified', modified]) + + report_utils.dump_report_data( + rows, + headers=['Name', 'Value'], + title=f'Record Revision V.{revision}', + no_header=True, + right_align=(0,) + ) + + def _show_diff(self, history: list, kwargs: dict): + """Show differences between revisions.""" + revision = kwargs.get('revision') or 0 + verbose = kwargs.get('verbose') or False + length = len(history) + + if revision < 0 or revision >= length: + raise ValueError(f'Invalid revision {revision}: valid revisions 1..{length - 1}') + + index = 0 if revision == 0 else length - revision + rows = self._generate_diff_rows(history, index, length, verbose) + + headers = ('Version', 'Field', 'New Value', 'Old Value') + report_utils.dump_report_data(rows, headers) + + def _generate_diff_rows(self, history: list, start_index: int, length: int, verbose: bool) -> list: + """Generate diff rows between revisions.""" + count = MAX_VERSION_COUNT + current = history[start_index].get('data_unencrypted') + current = json.loads(current) + rows = [] + index = start_index + + while count >= 0 and current: + previous = history[index + 1].get('data_unencrypted') if index < (length - 1) else None + previous = json.loads(previous) if previous else None + current_fields = self._get_record_fields(current) + previous_fields = self._get_record_fields(previous) if previous else {} + + last_pos = len(rows) + self._add_field_differences(rows, current_fields, previous_fields) + + version_label = 'Current' if index == 0 else f'V.{length - index}' + if len(rows) > last_pos: + rows[last_pos][0] = version_label + else: + rows.append([version_label, '', '', '']) + + count -= 1 + index += 1 + current = previous + + if not verbose: + self._truncate_long_values(rows) + + return rows + + def _get_record_fields(self, record: dict) -> dict: + """Get record fields as dictionary.""" + return_fields = {} + return_fields['Title'] = record.get('title') + for field in record.get('fields', []): + name = field.get('label') + if not name or name == '': + name = field.get('type') + value = field.get('value') + if isinstance(value, list): + value = '\n'.join(value) + return_fields[name] = value + return return_fields + + def _add_field_differences(self, rows: list, current_fields: dict, previous_fields: dict): + """Add field differences to rows.""" + for name, value in current_fields.items(): + if name in previous_fields: + pre_value = previous_fields[name] + if pre_value != value: + rows.append(['', name, value, pre_value]) + del previous_fields[name] + else: + if value: + rows.append(['', name, value, '']) + + for name, value in previous_fields.items(): + if value: + if isinstance(value, list): + value = '\n'.join(value) + rows.append(['', name, '', value]) + + def _truncate_long_values(self, rows: list): + """Truncate long values in diff rows for better readability.""" + for row in rows: + for index in (2, 3): + value = row[index] + if not value: + continue + lines = [x[:TRUNCATE_LENGTH-2]+'...' if len(x) > TRUNCATE_LENGTH else x for x in value.split('\n')] + if len(lines) > 3: + lines = lines[:2] + lines.append('...') + row[index] = '\n'.join(lines) + + def _restore_revision(self, vault: vault_online.VaultOnline, history: list, kwargs: dict): + """Restore a specific revision.""" + revision = kwargs.get('revision') or 0 + length = len(history) + + if revision == 0: + raise base.CommandError(f'Invalid revision to restore: Revisions: 1-{length - 1}') + + if revision < 0 or revision >= length: + raise ValueError(f'Invalid revision {revision}: valid revisions 1..{length - 1}') + + index = 0 if revision == 0 else length - revision + rev = history[index] + record_data_bytes = rev['data_unencrypted'] + record_data = json.loads(record_data_bytes) + + self._execute_restore_request(vault, rev['record_uid'], rev['revision']) + vault.client_audit_event_plugin().schedule_audit_event('revision_restored', record_uid=rev['record_uid']) + vault.sync_down() + logger.info('Record "%s" revision V.%d has been restored', record_data.get('title'), revision) + + def _execute_restore_request(self, vault: vault_online.VaultOnline, record_uid: str, revision: int): + """Execute the restore request to server.""" + r_uid = utils.base64_url_decode(record_uid) + roq = record_pb2.RecordRevert() + roq.record_uid = r_uid + roq.revert_to_revision = revision + + rq = record_pb2.RecordsRevertRequest() + rq.records.append(roq) + + rs = vault.keeper_auth.execute_auth_rest( + 'vault/records_revert', + rq, + response_type=record_pb2.RecordsModifyResponse + ) + + ros = next((x for x in rs.records if x.record_uid == r_uid), None) + if ros and ros.status != record_pb2.RS_SUCCESS: + raise base.CommandError(f'Failed to restore record "{record_uid}": {ros.message}') + diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 90b798f8..21862339 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -26,7 +26,8 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, - record_type, secrets_manager, share_management, password_report, trash, record_file_report) + record_type, secrets_manager, share_management, password_report, trash, record_file_report, + record_handling_commands) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -41,6 +42,9 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('list-team', vault_record.TeamListCommand(), base.CommandScope.Vault, 'lt') commands.register_command('shortcut', vault_record.ShortcutCommand(), base.CommandScope.Vault) commands.register_command('search', record_edit.RecordSearchCommand(), base.CommandScope.Vault, 's') + commands.register_command('record-history', record_handling_commands.RecordHistoryCommand(), base.CommandScope.Vault, 'rh') + commands.register_command('clipboard-copy', record_handling_commands.ClipboardCommand(), base.CommandScope.Vault, 'cc') + commands.register_command('find-password', record_handling_commands.ClipboardCommand(), base.CommandScope.Vault) commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') commands.register_command('record-update', record_edit.RecordUpdateCommand(), base.CommandScope.Vault, 'ru') commands.register_command('rm', record_edit.RecordDeleteCommand(), base.CommandScope.Vault) From fef509acdbc9840b9cb7afa8c8edb1d08d4067f3 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 8 Oct 2025 18:23:02 +0530 Subject: [PATCH 32/44] Audit log command added --- .../src/keepercli/commands/audit_log.py | 629 ++++++++++++++++++ .../src/keepercli/register_commands.py | 3 +- 2 files changed, 631 insertions(+), 1 deletion(-) create mode 100644 keepercli-package/src/keepercli/commands/audit_log.py diff --git a/keepercli-package/src/keepercli/commands/audit_log.py b/keepercli-package/src/keepercli/commands/audit_log.py new file mode 100644 index 00000000..78a81d0c --- /dev/null +++ b/keepercli-package/src/keepercli/commands/audit_log.py @@ -0,0 +1,629 @@ +import argparse +import datetime +import hashlib +import json +import sys +from typing import Any, Dict, List, Optional, Union + +from keepersdk.vault import vault_record, record_management +from .. import api +from .base import CommandError, ArgparseCommand +from ..params import KeeperParams +from ..prompt_utils import user_choice + + +logger = api.get_logger() + + +class RecordOperations: + """Handles operations on Keeper records for audit log configuration. + + This class provides static methods for getting and setting custom fields + on Keeper records, supporting both PasswordRecord and TypedRecord types. + """ + + @staticmethod + def get_custom_field(record: Union[vault_record.PasswordRecord, + vault_record.TypedRecord], + field_name: str) -> Optional[str]: + """Get custom field value from record.""" + if not hasattr(record, 'custom') or not record.custom: + return None + + if isinstance(record, vault_record.PasswordRecord): + for field in record.custom: + if field.name == field_name: + return field.value + elif isinstance(record, vault_record.TypedRecord): + for field in record.custom: + if field.label == field_name: + return field.value[0] if field.value else None + return None + + @staticmethod + def set_custom_field(record: Union[vault_record.PasswordRecord, + vault_record.TypedRecord], + field_name: str, value: str) -> None: + """Set custom field value in record.""" + if not hasattr(record, 'custom'): + record.custom = [] + + if isinstance(record, vault_record.PasswordRecord): + for field in record.custom: + if field.name == field_name: + field.value = value + return + + custom_field = vault_record.CustomField() + custom_field.name = field_name + custom_field.value = value + custom_field.type = 'text' + record.custom.append(custom_field) + + elif isinstance(record, vault_record.TypedRecord): + for field in record.custom: + if field.label == field_name: + field.value = [value] + return + + typed_field = vault_record.TypedField() + typed_field.type = 'text' + typed_field.label = field_name + typed_field.value = [value] + record.custom.append(typed_field) + + +class AuditLogExporter: + """Base class for audit log export functionality. + + This abstract base class defines the interface for audit log exporters. + Subclasses should implement the specific export format logic. + """ + + def __init__(self) -> None: + self.store_record: bool = False + self.should_cancel: bool = False + self.file_handle: Optional[Any] = None + + def get_default_record_title(self) -> str: + """Get the default title for the audit log record.""" + return 'Audit Log Export' + + def get_chunk_size(self) -> int: + """Get the chunk size for processing events.""" + return 1000 + + def get_properties(self, record: Union[vault_record.PasswordRecord, + vault_record.TypedRecord], + props: Dict[str, Any]) -> None: + """Extract properties from record for export context.""" + pass + + def convert_event(self, props: Dict[str, Any], + event: Dict[str, Any]) -> Dict[str, Any]: + """Convert an audit event to the export format.""" + return event + + def export_events(self, props: Dict[str, Any], + events: List[Dict[str, Any]]) -> None: + """Export a batch of events.""" + pass + + def finalize_export(self, props: Dict[str, Any]) -> None: + """Finalize the export process.""" + pass + + def clean_up(self) -> None: + """Clean up resources.""" + if self.file_handle: + self.file_handle.close() + self.file_handle = None + + +class JsonAuditLogExporter(AuditLogExporter): + """Handles JSON export of audit log events. + + This class exports audit log events to a JSON file format, + writing events in batches to handle large datasets efficiently. + """ + + def __init__(self, filename: str) -> None: + super().__init__() + self.filename: str = filename + self.events: List[Dict[str, Any]] = [] + self.file_handle: Optional[Any] = None + self.is_first_batch: bool = True + + def get_default_record_title(self) -> str: + """Get the default title for the audit log record.""" + return 'Audit Log: JSON' + + def _initialize_file(self) -> None: + """Initialize the JSON file for writing.""" + import os + if self.file_handle is None: + try: + self.file_handle = open(self.filename, 'w', encoding='utf-8') + self.file_handle.write('[\n') + logger.info('Creating audit log file: %s', os.path.basename(self.filename)) + except (IOError, OSError) as e: + raise CommandError(f'Failed to create file {self.filename}: {e}') + + def export_events(self, props: Dict[str, Any], + events: List[Dict[str, Any]]) -> None: + """Export a batch of events to JSON format.""" + self._initialize_file() + + try: + for i, event in enumerate(events): + if not self.is_first_batch or i > 0: + self.file_handle.write(',\n') + json.dump(event, self.file_handle, indent=2, + ensure_ascii=False) + self.is_first_batch = False + + self.file_handle.flush() + self.events.extend(events) + except (IOError, OSError) as e: + raise CommandError(f'Failed to write to file {self.filename}: {e}') + + def finalize_export(self, props: Dict[str, Any]) -> None: + """Finalize the JSON export by closing the array.""" + import os + if self.file_handle: + try: + self.file_handle.write('\n]') + self.file_handle.close() + self.file_handle = None + except (IOError, OSError) as e: + logger.error('Failed to finalize export: %s', e) + raise CommandError(f'Failed to finalize export: {e}') + + logger.info('Audit log exported to: %s', os.path.basename(self.filename)) + + def clean_up(self) -> None: + """Clean up file resources.""" + if self.file_handle: + self.file_handle.close() + self.file_handle = None + + +class FilterManager: + """Manages audit log filtering and configuration. + + This class handles loading filter settings from Keeper records, + applying command-line filter overrides, and building API request filters. + """ + + def __init__(self, record: Union[vault_record.PasswordRecord, + vault_record.TypedRecord]) -> None: + self.record = record + self.shared_folder_uids: Optional[List[str]] = None + self.node_ids: Optional[List[int]] = None + self.days: Optional[int] = None + self.last_event_time: int = 0 + + def load_filters_from_record(self) -> None: + """Load filter settings from the record.""" + # Load shared folder UIDs + val = RecordOperations.get_custom_field( + self.record, 'shared_folder_uids' + ) + if val: + try: + self.shared_folder_uids = [ + sfuid.strip() for sfuid in val.split(',') + if sfuid.strip() + ] + except (ValueError, AttributeError) as e: + logger.warning('Failed to parse shared folder UIDs: %s', e) + self.shared_folder_uids = None + + # Load node IDs + val = RecordOperations.get_custom_field(self.record, 'node_ids') + if val: + try: + self.node_ids = [ + int(node_id.strip()) for node_id in val.split(',') + if node_id.strip() + ] + except (ValueError, AttributeError) as e: + logger.warning('Failed to parse node IDs: %s', e) + self.node_ids = None + + # Load last event time + val = RecordOperations.get_custom_field(self.record, 'last_event_time') + if val: + try: + self.last_event_time = int(val) + except (ValueError, TypeError) as e: + logger.warning('Failed to parse last event time: %s', e) + self.last_event_time = 0 + + def apply_command_line_filters(self, shared_folder_uids: Optional[List[str]], + node_ids: Optional[List[int]], + days: Optional[int]) -> None: + """Apply filters from command line arguments.""" + if shared_folder_uids: + self.shared_folder_uids = shared_folder_uids + if node_ids: + self.node_ids = node_ids + if days: + if days <= 0: + raise CommandError('Days must be a positive integer') + self.days = days + now_dt = datetime.datetime.now() + last_event_dt = now_dt - datetime.timedelta(days=int(days)) + self.last_event_time = int(last_event_dt.timestamp()) + + def build_request_filter(self, now_ts: int) -> Dict[str, Any]: + """Build the filter for the audit log request.""" + created_filter = {'max': now_ts} + rq_filter = {'created': created_filter} + + if self.shared_folder_uids: + rq_filter['shared_folder_uid'] = self.shared_folder_uids + RecordOperations.set_custom_field( + self.record, 'shared_folder_uids', + ', '.join(self.shared_folder_uids) + ) + + if self.node_ids: + rq_filter['node_id'] = self.node_ids + node_ids_str = [str(n) for n in self.node_ids] + RecordOperations.set_custom_field( + self.record, 'node_ids', ', '.join(node_ids_str) + ) + + return rq_filter + + def save_last_event_time(self, last_event_time: int) -> None: + """Save the last event time to the record.""" + if last_event_time > 0: + RecordOperations.set_custom_field( + self.record, 'last_event_time', str(last_event_time) + ) + + +class AuditEventFetcher: + """Handles fetching audit events from the Keeper API. + + This class manages the process of fetching audit events from the Keeper API, + including pagination, filtering, and anonymization of user data. + """ + + LIMIT : int = 1000 + def __init__(self, context: KeeperParams, filter_manager: FilterManager) -> None: + self.context = context + self.filter_manager = filter_manager + self.ent_user_ids: Dict[str, str] = {} + + def setup_anonymization(self, anonymize: bool) -> None: + """Setup user ID mapping for anonymization.""" + if anonymize and self.context.enterprise_data: + self.ent_user_ids = { + user.username: user.enterprise_user_id + for user in self.context.enterprise_data.users.get_all_entities() + } + + def get_total_events_count(self, now_ts: int) -> int: + """Get the total number of events to be exported.""" + created_filter_copy = { + **self.filter_manager.build_request_filter(now_ts)['created'], + 'min': self.filter_manager.last_event_time + } + filter_copy = { + **self.filter_manager.build_request_filter(now_ts), + 'created': created_filter_copy + } + + total_events_rq = { + 'command': 'get_audit_event_reports', + 'report_type': 'span', + 'scope': 'enterprise', + 'limit': self.LIMIT, + 'order': 'ascending', + 'filter': filter_copy + } + + try: + total_events_rs = self.context.auth.execute_auth_command(total_events_rq) + rows = total_events_rs['audit_event_overview_report_rows'] + return rows[0].get('occurrences', 0) if rows else 0 + except (KeyError, IndexError, TypeError): + logger.info('No events to export') + return 0 + + def anonymize_event(self, event: Dict[str, Any]) -> None: + """Anonymize user information in an event.""" + uname = (event.get('email') or event.get('username') or '') + if uname: + ent_uid = self._resolve_uid(uname) + event['username'] = ent_uid + event['email'] = ent_uid + + to_uname = event.get('to_username') or '' + if to_uname: + event['to_username'] = self._resolve_uid(to_uname) + + from_uname = event.get('from_username') or '' + if from_uname: + event['from_username'] = self._resolve_uid(from_uname) + + def _resolve_uid(self, username: str) -> str: + """Resolve username to enterprise user ID or generate deleted user ID.""" + uname = username or '' + uid = self.ent_user_ids.get(uname) + if not uid: + md5 = hashlib.md5(str(uname).encode('utf-8')).hexdigest() + self.ent_user_ids[uname] = 'DELETED-' + md5 + uid = self.ent_user_ids[uname] + return uid + + def fetch_events(self, now_ts: int, anonymize: bool = False) -> List[Dict[str, Any]]: + """Fetch all audit events matching the current filters.""" + events = [] + finished = False + logged_ids = set() + last_event_time = self.filter_manager.last_event_time + + rq_filter = self.filter_manager.build_request_filter(now_ts) + rq = { + 'command': 'get_audit_event_reports', + 'report_type': 'raw', + 'scope': 'enterprise', + 'limit': self.LIMIT, + 'order': 'ascending', + 'filter': rq_filter + } + + while not finished: + finished = True + + if last_event_time > 0: + rq_filter['created']['min'] = last_event_time + + response = self.context.auth.execute_auth_command(rq) + if response['result'] == 'success': + finished = True + if 'audit_event_overview_report_rows' in response: + audit_events = response['audit_event_overview_report_rows'] + event_count = len(audit_events) + + if event_count > 0: + last_event_time = int(audit_events[-1]['created']) + + new_events = [ + e for e in audit_events if e['id'] not in logged_ids + ] + + if anonymize and new_events: + for event in new_events: + self.anonymize_event(event) + + for event in new_events: + logged_ids.add(event['id']) + events.append(event) + + if event_count < self.LIMIT: + finished = True + else: + finished = rq_filter['created']['max'] <= last_event_time + + if not new_events and not finished: + last_event_time += 1 + + # Update the filter manager with the last event time + self.filter_manager.last_event_time = last_event_time + return events + + +class AuditLogCommand(ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='audit-log', + description='Export and display the enterprise audit log' + ) + AuditLogCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '--anonymize', + action='store_true', + help="Anonymizes audit log by replacing email and user name " + "with corresponding enterprise user id. If user was removed " + "or if user's email was changed then the audit report will " + "show that particular entry as deleted user." + ) + parser.add_argument( + '--target', + choices=['json'], + help='Target for audit log export' + ) + parser.add_argument( + '--record', + dest='Record', + help='Keeper record name or UID' + ) + parser.add_argument( + '--shared-folder-uid', + dest='shared_folder_uid', + action='append', + help='Filter: Shared Folder UID(s). Overrides existing setting ' + 'in config record and sets new field value.' + ) + parser.add_argument( + '--node-id', + dest='node_id', + action='append', + type=int, + help='Filter: Node ID(s). Overrides existing setting in config ' + 'record and sets new field value.' + ) + parser.add_argument( + '--days', + type=int, + help='Filter: max event age in days. Overrides existing ' + '"last_event_time" value in config record' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the audit log export command.""" + self._validate_context(context) + target = self._validate_target(kwargs.get('target')) + + log_export = self._setup_exporter() + record = self._setup_record(context, log_export, kwargs.get('Record')) + filter_manager = self._setup_filter_manager(record, kwargs) + event_fetcher = self._setup_event_fetcher(context, filter_manager, kwargs) + + total_events = self._get_total_events_count(event_fetcher) + if total_events == 0: + return + + self._process_and_export_events(event_fetcher, log_export, total_events, kwargs) + self._finalize_export(filter_manager, record, context, log_export) + + def _validate_context(self, context: KeeperParams) -> None: + """Validate that required context components are available.""" + assert context.auth + assert context.enterprise_data + + def _validate_target(self, target: Optional[str]) -> str: + """Validate and return the target format.""" + if not target: + raise CommandError('Target is required') + if target != 'json': + raise CommandError(f'Target {target} not yet implemented') + return target + + def _setup_exporter(self) -> JsonAuditLogExporter: + """Setup the audit log exporter.""" + filename = self._get_filename() + return JsonAuditLogExporter(filename) + + def _setup_record(self, context: KeeperParams, log_export: JsonAuditLogExporter, + record_name: Optional[str]) -> Union[vault_record.PasswordRecord, vault_record.TypedRecord]: + """Setup the audit log record.""" + return self._find_or_create_record(context, log_export, record_name) + + def _setup_filter_manager(self, record: Union[vault_record.PasswordRecord, vault_record.TypedRecord], + kwargs: Dict[str, Any]) -> FilterManager: + """Setup and configure the filter manager.""" + filter_manager = FilterManager(record) + filter_manager.load_filters_from_record() + filter_manager.apply_command_line_filters( + kwargs.get('shared_folder_uid'), + kwargs.get('node_id'), + kwargs.get('days') + ) + return filter_manager + + def _setup_event_fetcher(self, context: KeeperParams, filter_manager: FilterManager, + kwargs: Dict[str, Any]) -> AuditEventFetcher: + """Setup the audit event fetcher.""" + event_fetcher = AuditEventFetcher(context, filter_manager) + event_fetcher.setup_anonymization(bool(kwargs.get('anonymize'))) + return event_fetcher + + def _get_total_events_count(self, event_fetcher: AuditEventFetcher) -> int: + """Get the total number of events to export.""" + now_ts = int(datetime.datetime.now().timestamp()) + return event_fetcher.get_total_events_count(now_ts) + + def _process_and_export_events(self, event_fetcher: AuditEventFetcher, + log_export: JsonAuditLogExporter, + total_events: int, kwargs: Dict[str, Any]) -> None: + """Process and export audit events.""" + now_ts = int(datetime.datetime.now().timestamp()) + anonymize = bool(kwargs.get('anonymize')) + events = event_fetcher.fetch_events(now_ts, anonymize) + self._export_events_in_chunks(log_export, events, total_events) + + def _finalize_export(self, filter_manager: FilterManager, + record: Union[vault_record.PasswordRecord, vault_record.TypedRecord], + context: KeeperParams, log_export: JsonAuditLogExporter) -> None: + """Finalize the export process.""" + if filter_manager.last_event_time > 0: + filter_manager.save_last_event_time(filter_manager.last_event_time) + record_management.update_record(context.vault, record) + context.sync_data = True + + log_export.clean_up() + + def _get_filename(self) -> str: + """Get filename from user input.""" + filename = input('JSON File name: ').strip() + if not filename: + raise CommandError('Filename is required. Command cancelled.') + + if not filename.lower().endswith('.json'): + filename += '.json' + + return filename + + def _find_or_create_record(self, context: KeeperParams, + log_export: JsonAuditLogExporter, + record_name: Optional[str]) -> Union[vault_record.PasswordRecord, + vault_record.TypedRecord]: + """Find existing record or create new one.""" + if not record_name: + record_name = log_export.get_default_record_title() + + # Look for existing record + for record_info in context.vault.vault_data.records(): + rec = context.vault.vault_data.load_record(record_info.record_uid) + if record_name in [rec.record_uid, rec.title]: + return rec + + # Create new record if not found + answer = user_choice( + 'Do you want to create a Keeper record to store audit log ' + 'settings?', 'yn', 'n' + ) + if answer.lower() in ('y', 'yes'): + record_title = input( + f'Choose the title for audit log record ' + f'[Default: {record_name}]: ' + ) or log_export.get_default_record_title() + + record = vault_record.PasswordRecord() + record.title = record_title + record_management.add_record_to_folder(context.vault, record) + record_uid = record.record_uid + if record_uid: + context.vault.sync_down() + return context.vault.vault_data.load_record(record_uid) + + raise CommandError('Record not found') + + def _export_events_in_chunks(self, log_export: JsonAuditLogExporter, + events: List[Dict[str, Any]], + total_events: int) -> None: + """Export events in chunks with progress indication.""" + props = {'enterprise_name': 'Unknown'} # Could be enhanced to get from context + chunk_length = log_export.get_chunk_size() + num_exported = 0 + + while len(events) > 0: + to_store = events[:chunk_length] + events = events[chunk_length:] + log_export.export_events(props, to_store) + + if log_export.should_cancel: + break + + num_exported += len(to_store) + if total_events > 0: + percent_done = num_exported / total_events * 100 + percent_done = '%.1f' % percent_done + print(f'Exporting events.... {percent_done}% DONE', + file=sys.stderr, end='\r', flush=True) + + logger.info('') + logger.info('Exported %d audit event(s)', num_exported) + + if num_exported > 0: + log_export.finalize_export(props) diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 21862339..91f814f0 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -76,7 +76,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Enterprise): from .commands import (enterprise_info, enterprise_node, enterprise_role, enterprise_team, enterprise_user, - importer_commands, audit_report, audit_alert) + importer_commands, audit_report, audit_alert, audit_log) commands.register_command('enterprise-down', enterprise_info.EnterpriseDownCommand(), base.CommandScope.Enterprise, 'ed') commands.register_command('enterprise-info', enterprise_info.EnterpriseInfoCommand(), base.CommandScope.Enterprise, 'ei') @@ -86,5 +86,6 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('enterprise-user', enterprise_user.EnterpriseUserCommand(), base.CommandScope.Enterprise, 'eu') commands.register_command('audit-report', audit_report.EnterpriseAuditReport(), base.CommandScope.Enterprise) commands.register_command('audit-alert', audit_alert.AuditAlerts(), base.CommandScope.Enterprise) + commands.register_command('audit-log', audit_log.AuditLogCommand(), base.CommandScope.Enterprise, 'al') commands.register_command('download-membership', importer_commands.DownloadMembershipCommand(), base.CommandScope.Enterprise) commands.register_command('apply-membership', importer_commands.ApplyMembershipCommand(), base.CommandScope.Enterprise) \ No newline at end of file From 539d3af1ad2526da3615263c4e0a43337f54780e Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 10 Oct 2025 18:53:05 +0530 Subject: [PATCH 33/44] Read me update --- README.md | 64 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0403b1df..ac29dd2a 100644 --- a/README.md +++ b/README.md @@ -15,42 +15,56 @@ pip install keepersdk $ git clone https://github.com/Keeper-Security/keeper-sdk-python ``` +### Steps to setup environment for using python keepersdk +``` +Requirement - python 3.10 or higher +1. Open a terminal/zsh/powershell +2. Create a virtual environment (venv) using "python3 -m venv venv" (Optionally python or py depending on python setup) +3. Activate the venv using "source venv/bin/activate" for MacOS/Linux or "venv\Scripts\Activate' for Windows +4. Move to keepersdk-package using "cd keepersdk-package" +5. Run "pip install -r requirements.txt" for installing dependencies +6. Run "pip install setuptools" tp install setuptools which will be used to create keepersdk package +7. Run "python setup.py install" to install keepersdk from the keepersdk-package as a lib +8. Create a client file for the keepersdk to use it complete the login flow and access Keeper Vault and Console elements. An example is added below. +``` + + +### Steps to setup enviroment for using python keepercli-package +``` +Continuing from step 7 of previous section to setup keepersdk-package +8. Move to keepercli-package using "cd ../keepercli-package" or "cd ..\keepercli-package" +9. Run "pip install -r requirements.txt" for installing dependencies +10. Run "python setup.py install" to install keepercli from the keepercli-package as a lib +11. Run command "python -m keepercli" to run the keepercli which is the new version of Commander CLI with more efficient commands +``` + ### Example ```python -import os -from login import auth, configuration, endpoint -from vault import sqlite_storage, vault_online, vault_record +import sqlite3 -config_filename = os.path.join(os.path.dirname(__file__), 'config.json') -config = configuration.JsonConfigurationStorage(file_name=config_filename) +from keepersdk.authentication import login_auth, configuration, endpoint +from keepersdk.vault import sqlite_storage, vault_online, vault_record + +config = configuration.JsonConfigurationStorage() keeper_endpoint = endpoint.KeeperEndpoint(config) -login_auth = auth.LoginAuth(keeper_endpoint) -login_auth.login('username@company.com') - -while not login_auth.login_step.is_final(): - if isinstance(login_auth.login_step, auth.LoginStepPassword): - password = input('Enter password: ') - login_auth.login_step.verify_password(password) - if isinstance(login_auth.login_step, auth.LoginStepTwoFactor): - channel = login_auth.login_step.get_channels()[0] - code = input(f'Enter 2FA code for {channel.channel_name}: ') - login_auth.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError() - -if isinstance(login_auth.login_step, auth.LoginStepConnected): - keeper_auth = login_auth.login_step.keeper_auth() - vault_storage = sqlite_storage.SqliteVaultStorage(file_name=':memory:', - vault_owner=keeper_auth.auth_context.username) +login_auth_context = login_auth.LoginAuth(keeper_endpoint) +login_auth_context.login('username@company.com') +# bypassing device approval and 2fa step, not recommended +login_auth_context.login_step.verify_password('yourpassword') + +if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + keeper_auth = login_auth_context.login_step.take_keeper_auth() + conn = sqlite3.Connection('file::memory:', uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage(lambda: conn, vault_owner=bytes(keeper_auth.auth_context.username, 'utf-8')) vault = vault_online.VaultOnline(keeper_auth, vault_storage) vault.sync_down() # List records - for record in vault.records(): + for record in vault.vault_data.records(): print(f'Title: {record.title}') if record.version == 2: - legacy_record = vault.load_record(record.record_uid) + legacy_record = vault.vault_data.load_record(record.record_uid) if isinstance(legacy_record, vault_record.PasswordRecord): print(f'Username: {legacy_record.login}') ``` \ No newline at end of file From 4f1035b0532640179e1a9b8299bbf91286a4279e Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 10 Oct 2025 19:13:17 +0530 Subject: [PATCH 34/44] Transform folder command added --- .../src/keepercli/commands/register.py | 218 ++++++ .../keepercli/commands/share_management.py | 31 +- .../src/keepercli/commands/vault_folder.py | 741 +++++++++++++++++- .../src/keepercli/helpers/share_utils.py | 376 +++++++-- .../src/keepercli/register_commands.py | 4 +- 5 files changed, 1295 insertions(+), 75 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/register.py diff --git a/keepercli-package/src/keepercli/commands/register.py b/keepercli-package/src/keepercli/commands/register.py new file mode 100644 index 00000000..c3fdf749 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/register.py @@ -0,0 +1,218 @@ +import argparse + +from keepersdk import crypto, utils +from keepersdk.proto import APIRequest_pb2 +from keepersdk.vault import vault_utils + +from . import base +from .. import api +from ..helpers import report_utils, share_utils +from ..params import KeeperParams + +CHUNK_SIZE = 1000 +OWNERLESS_RECORDS_GET_ENDPOINT = 'ownerless_records/get_records' +OWNERLESS_RECORDS_SET_OWNER_ENDPOINT = 'ownerless_records/set_owner' +DEFAULT_OUTPUT_FORMAT = 'table' +DEFAULT_VERBOSE_THRESHOLD = 0 # When verbose should be enabled by default + +logger = api.get_logger() + + +class FindOwnerlessCommand(base.ArgparseCommand): + """ + Command to find and optionally claim ownerless records in the vault. + + This command identifies records that don't have an owner and can optionally + claim them for the current user. + """ + + def __init__(self): + parser = argparse.ArgumentParser( + prog='find-ownerless', + description='List (and, optionally, claim) records in the user\'s vault that currently do not have an owner', + parents=[base.report_output_parser] + ) + FindOwnerlessCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command-specific arguments to the parser.""" + parser.add_argument( + '--claim', + dest='claim', + action='store_true', + help='Claim the found records as the owner' + ) + parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Output detailed information for each record found' + ) + parser.add_argument( + 'folder', + nargs='*', + type=str, + action='store', + help='Path or UID of folder to search (optional, multiple values allowed)' + ) + parser.error = base.ArgparseCommand.raise_parse_exception + parser.exit = base.ArgparseCommand.suppress_exit + + def execute(self, context: KeeperParams, **kwargs): + """Execute the find-ownerless command.""" + assert context.vault is not None + vault = context.vault + + claim_records = kwargs.get('claim', False) + output_format = kwargs.get('format', DEFAULT_OUTPUT_FORMAT) + output_file = kwargs.get('output') + verbose = kwargs.get('verbose', False) or not claim_records or output_file + folders = kwargs.get('folder', []) + + ownerless_records = self._fetch_ownerless_records(vault) + + if folders and ownerless_records: + ownerless_records = self._filter_records_by_folders(context, ownerless_records, folders) + + if ownerless_records: + logger.info(f'Found [{len(ownerless_records)}] ownerless record(s)') + + if verbose: + records_dump = self._dump_record_details( + context, ownerless_records, output_file, output_format + ) + else: + records_dump = None + + if claim_records: + self._claim_ownerless_records(vault, ownerless_records) + vault.sync_down(force=True) + logger.info('Records have been claimed successfully') + else: + logger.info('To claim the record(s) found above, re-run this command with the --claim flag.') + + return records_dump + else: + logger.info('No ownerless records found') + return None + + def _fetch_ownerless_records(self, vault): + """Fetch ownerless records from the API.""" + try: + request = APIRequest_pb2.OwnerlessRecords() + response = vault.keeper_auth.execute_auth_rest( + request=request, + rest_endpoint=OWNERLESS_RECORDS_GET_ENDPOINT, + response_type=APIRequest_pb2.OwnerlessRecords + ) + + if not response or not response.ownerlessRecord: + return [] + + record_uids = {utils.base64_url_encode(rec.recordUid) for rec in response.ownerlessRecord if rec} + records = [vault.vault_data.get_record(uid) for uid in record_uids] + + return [record for record in records if record is not None] + + except Exception as e: + logger.error(f"Failed to fetch ownerless records: {e}") + return [] + + def _filter_records_by_folders(self, context, records, folders): + """Filter records to only include those in the specified folders.""" + folder_record_uids = set() + for folder_path in folders: + contained_records = share_utils.get_contained_record_uids(context, folder_path, False) + for record_uids in contained_records.values(): + folder_record_uids.update(record_uids) + + return [record for record in records if record.record_uid in folder_record_uids] + + def _create_ownerless_record_request(self, vault, records): + """Create API request parameters for ownerless records.""" + request_params = [] + + for record in records: + try: + record_key = vault.vault_data.get_record_key(record.record_uid) + encrypted_key = crypto.encrypt_aes_v1(record_key, vault.keeper_auth.auth_context.data_key) + + ownerless_record = APIRequest_pb2.OwnerlessRecord() + ownerless_record.recordUid = utils.base64_url_decode(record.record_uid) + ownerless_record.recordKey = encrypted_key + + request_params.append(ownerless_record) + + except Exception as e: + logger.warning(f"Failed to prepare record {record.record_uid} for claiming: {e}") + + return request_params + + def _claim_ownerless_records(self, vault, records): + """Claim the specified ownerless records.""" + if not records: + return + + chunk_size = CHUNK_SIZE + total_claimed = 0 + + for i in range(0, len(records), chunk_size): + chunk = records[i:i + chunk_size] + request_params = self._create_ownerless_record_request(vault, chunk) + + if not request_params: + continue + + try: + request = APIRequest_pb2.OwnerlessRecords() + request.ownerlessRecord.extend(request_params) + + vault.keeper_auth.execute_auth_rest( + request=request, + rest_endpoint=OWNERLESS_RECORDS_SET_OWNER_ENDPOINT, + response_type=APIRequest_pb2.OwnerlessRecords + ) + + total_claimed += len(request_params) + logger.debug(f"Claimed {len(request_params)} records in chunk {i//chunk_size + 1}") + + except Exception as e: + logger.error(f"Failed to claim records in chunk {i//chunk_size + 1}: {e}") + + def _dump_record_details(self, context, records, output_file, output_format): + """Generate detailed report of ownerless records.""" + if not records: + return None + + record_uids = {record.record_uid for record in records} + shared_records = share_utils.get_shared_records(context, record_uids).values() + + headers = ['record_uid', 'title', 'shared_with', 'folder_path'] + table_data = [] + + for shared_record in shared_records: + folder_paths = vault_utils.get_folders_for_record(context.vault.vault_data, shared_record.record_uid) + folder_path = '/'.join([folder.name for folder in folder_paths]) if folder_paths else '' + + admin_usernames = shared_record.get_all_share_admins() + + row = [ + shared_record.record_uid, + shared_record.title, + admin_usernames, + folder_path + ] + table_data.append(row) + + if output_format != 'json': + headers = [report_utils.field_to_title(header) for header in headers] + + return report_utils.dump_report_data( + table_data, + headers, + fmt=output_format, + filename=output_file, + row_number=True + ) + diff --git a/keepercli-package/src/keepercli/commands/share_management.py b/keepercli-package/src/keepercli/commands/share_management.py index a49abefa..cd47451d 100644 --- a/keepercli-package/src/keepercli/commands/share_management.py +++ b/keepercli-package/src/keepercli/commands/share_management.py @@ -607,31 +607,6 @@ def get_share_admin_obj_uids(vault: vault_online.VaultOnline, obj_names, obj_typ except Exception as e: raise ValueError(f'get_share_admin: msg = {e}') - def get_folder_uids(context: KeeperParams, name: str) -> set[str]: - """Get folder UIDs by name or path.""" - folder_uids = set() - - if not context.vault or not context.vault.vault_data: - return folder_uids - - if name in context.vault.vault_data._folders: - folder_uids.add(name) - return folder_uids - - for folder in context.vault.vault_data.folders(): - if folder.name == name: - folder_uids.add(folder.folder_uid) - - if not folder_uids: - try: - folder, _ = folder_utils.try_resolve_path(context, name) - if folder: - folder_uids.add(folder.folder_uid) - except: - pass - - return folder_uids - def get_record_uids(context: KeeperParams, name: str) -> set[str]: """Get record UIDs by name or UID.""" record_uids = set() @@ -668,7 +643,7 @@ def get_record_uids(context: KeeperParams, name: str) -> set[str]: folder_uids = { uid for name in names if name - for uid in get_folder_uids(context, name) + for uid in share_utils.get_folder_uids(context, name) } folders = {get_folder_by_uid(uid) for uid in folder_uids if get_folder_by_uid(uid)} shared_folder_uids.update([uid for uid in folder_uids if uid in shared_folder_cache]) @@ -676,7 +651,7 @@ def get_record_uids(context: KeeperParams, name: str) -> set[str]: sf_subfolders = {f for f in folders if f and f.folder_type == 'shared_folder_folder'} shared_folder_uids.update({f.folder_scope_uid for f in sf_subfolders if f.folder_scope_uid}) - unresolved_names = [name for name in names if name and not get_folder_uids(context, name)] + unresolved_names = [name for name in names if name and not share_utils.get_folder_uids(context, name)] share_admin_folder_uids = get_share_admin_obj_uids(vault=vault, obj_names=unresolved_names, obj_type=record_pb2.CHECK_SA_ON_SF) shared_folder_uids.update(share_admin_folder_uids or []) @@ -1117,7 +1092,7 @@ def on_folder(f): folder = vault.vault_data.get_folder(folder_uid=folder_uid) if recursive: - vault_utils.traverse_folder_tree(vault, folder_uid, on_folder) + vault_utils.traverse_folder_tree(vault.vault_data, folder, on_folder) else: on_folder(folder) diff --git a/keepercli-package/src/keepercli/commands/vault_folder.py b/keepercli-package/src/keepercli/commands/vault_folder.py index 9b8f730d..bc63d79c 100644 --- a/keepercli-package/src/keepercli/commands/vault_folder.py +++ b/keepercli-package/src/keepercli/commands/vault_folder.py @@ -2,6 +2,8 @@ import fnmatch import functools import itertools +import json +import logging import re import shutil from collections import OrderedDict @@ -9,8 +11,10 @@ from asciitree import LeftAligned from colorama import Style +from keepersdk.proto import folder_pb2 +from keepersdk import crypto, utils -from keepersdk.vault import vault_types, vault_record, folder_management, record_management, vault_utils +from keepersdk.vault import vault_data, vault_types, vault_record, folder_management, record_management, vault_utils, vault_online from . import base from .. import prompt_utils, constants, api from ..helpers import folder_utils, report_utils @@ -27,7 +31,7 @@ def resolve_single_folder(folder_name: Optional[str], context: KeeperParams): if not folder: folder, pattern = folder_utils.try_resolve_path(context, folder_name) if pattern: - folder = None + folder = None if not folder: raise base.CommandError(f'Folder "{folder_name}" not found') @@ -542,3 +546,736 @@ def on_warning(message: str): is_link=kwargs.get('link') is True, can_edit=can_edit, can_share=can_share, on_warning=on_warning) + + +class FolderTransformCommand(base.ArgparseCommand, _FolderMixin): + # Constants + MAX_RECORDS_PER_BATCH = 1000 + MAX_FOLDERS_PER_CHUNK = 990 + MAX_DELETE_CHUNK_SIZE = 450 + DELETE_SUFFIX = '@delete' + FOLDER_TYPES = { + 'user_folder': 'user_folder', + 'shared_folder': 'shared_folder', + 'shared_folder_folder': 'shared_folder_folder' + } + FOLDER_TYPE_CHOICES = ['personal', 'shared'] + CONFIRMATION_CHOICES = 'yn' + DEFAULT_CONFIRMATION = 'n' + + def __init__(self): + self.parser = argparse.ArgumentParser(prog='transform-folder', description='Move folders to another location') + FolderTransformCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('folder', nargs='+', type=str, action='store', metavar='FOLDER', + help='folder path or UID (can specify multiple folders)') + parser.add_argument('-t', '--target', type=str, + help='target folder UID or path/name (root folder if not specified)') + parser.add_argument('-f', '--force', action='store_true', + help='Skip confirmation prompt and minimize output') + parser.add_argument('--link', action='store_true', + help='Do not delete the source folder(s)') + parser.add_argument('--dry-run', action='store_true', + help='Dry run mode: do not apply any changes') + parser.add_argument('--folder-type', choices=FolderTransformCommand.FOLDER_TYPE_CHOICES, + help='Folder type: Personal or Shared if target folder parameter is omitted') + + @staticmethod + def _get_folder_encryption_key(folder): + """Get the encryption key for a folder based on its type.""" + if folder.folder_type == FolderTransformCommand.FOLDER_TYPES['user_folder']: + return folder.folder_key + elif folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + return folder.folder_key + elif folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder_folder']: + return folder.folder_key + return None + + @staticmethod + def _create_rename_request(folder_uid, folder): + """Create a rename request for a folder.""" + encryption_key = FolderTransformCommand._get_folder_encryption_key(folder) + if not encryption_key: + return None + + rq = { + 'command': 'folder_update', + 'folder_uid': folder_uid, + 'folder_type': folder.folder_type, + } + + # Add shared folder UID for shared folder types + if folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + rq['shared_folder_uid'] = folder_uid + elif folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder_folder']: + rq['shared_folder_uid'] = folder.folder_scope_uid + + # Create encrypted data with delete suffix + new_name = f'{folder.name}{FolderTransformCommand.DELETE_SUFFIX}' + data = {'name': new_name} + encrypted_data = crypto.encrypt_aes_v1(json.dumps(data).encode(), encryption_key) + rq['data'] = utils.base64_url_encode(encrypted_data) + + # Add encrypted name for shared folders + if folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + rq['name'] = utils.base64_url_encode(crypto.encrypt_aes_v1(new_name.encode('utf-8'), encryption_key)) + + return rq + + @staticmethod + def rename_source_folders(vault: vault_online.VaultOnline, source_folders): + """Rename source folders by appending @delete to mark them for deletion.""" + rename_rqs = [] + + for folder_uid in source_folders: + folder = vault.vault_data.get_folder(folder_uid) + if not folder: + continue + + rename_rq = FolderTransformCommand._create_rename_request(folder_uid, folder) + if rename_rq: + rename_rqs.append(rename_rq) + + if rename_rqs: + try: + vault.keeper_auth.execute_batch(rename_rqs) + except Exception as e: + logging.debug('Error renaming source folders: %s', e) + + @staticmethod + def _get_folder_scope(folder): + """Get the scope UID for a folder based on its type.""" + if folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + return folder.folder_uid + elif folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder_folder']: + return folder.folder_scope_uid + return '' + + @staticmethod + def _get_scope_key(vault, dst_scope): + """Get the encryption key for a destination scope.""" + if dst_scope: + shared_folder = vault.vault_data.get_folder(dst_scope) + return shared_folder.folder_key + return vault.keeper_auth.auth_context.data_key + + @staticmethod + def _get_source_type(src_folder): + """Get the source type string for a folder.""" + if src_folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + return 'shared_folder' + elif src_folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder_folder']: + return 'shared_folder_folder' + return 'user_folder' + + @staticmethod + def _get_record_permissions(vault, sf_uid, r_uid, record_permissions_cache): + """Get record permissions for a record in a shared folder.""" + if sf_uid in record_permissions_cache: + return record_permissions_cache[sf_uid].get(r_uid) + + shared_folder_details = vault.vault_data.get_folder(sf_uid) + if not shared_folder_details: + return None + + record_permissions_cache[sf_uid] = {} + shared_folder = vault.vault_data.load_shared_folder(shared_folder_uid=sf_uid) + + for record_uid in shared_folder_details.records: + record_perm = shared_folder.record_permissions.get(record_uid) + if record_perm: + can_share = record_perm.can_share or False + can_edit = record_perm.can_edit or False + record_permissions_cache[sf_uid][record_uid] = (can_edit, can_share) + + return record_permissions_cache[sf_uid].get(r_uid) + + @staticmethod + def _create_transition_key(vault, record_uid, scope_key): + """Create a transition key for moving a record between scopes.""" + record = vault.vault_data.get_record(record_uid) + if not record: + return None + + record_key = vault.vault_data.get_record_key(record_uid) + if record.version < 3: + transfer_key = crypto.encrypt_aes_v1(record_key, scope_key) + else: + transfer_key = crypto.encrypt_aes_v2(record_key, scope_key) + + return { + 'uid': record_uid, + 'key': utils.base64_url_encode(transfer_key) + } + + @staticmethod + def _create_move_request(dst_folder, dst_scope, is_link): + """Create a base move request structure.""" + return { + 'command': 'move', + 'to_type': 'shared_folder_folder' if dst_scope else 'user_folder', + 'to_uid': dst_folder.folder_uid, + 'link': is_link, + 'move': [], + 'transition_keys': [] + } + + @staticmethod + def _process_record_chunk(vault, chunk, src_folder, dst_folder, src_scope, dst_scope, + scope_key, src_type, is_link, record_permissions_cache): + """Process a chunk of records and create move requests.""" + move_rqs = [] + records = list(chunk) + + while records: + rq = FolderTransformCommand._create_move_request(dst_folder, dst_scope, is_link) + record_chunk = records[:FolderTransformCommand.MAX_FOLDERS_PER_CHUNK] + records = records[FolderTransformCommand.MAX_FOLDERS_PER_CHUNK:] + + for record_uid in record_chunk: + move = { + 'type': 'record', + 'uid': record_uid, + 'from_type': src_type, + 'from_uid': src_folder.folder_uid, + 'cascade': False, + } + + # Add permissions if moving between different scopes + if scope_key and src_scope and dst_scope: + perms = FolderTransformCommand._get_record_permissions( + vault, src_scope, record_uid, record_permissions_cache) + if isinstance(perms, tuple): + move['can_edit'] = perms[0] + move['can_reshare'] = perms[1] + + rq['move'].append(move) + + # Add transition key if needed + if scope_key: + transition_key = FolderTransformCommand._create_transition_key( + vault, record_uid, scope_key) + if transition_key: + rq['transition_keys'].append(transition_key) + + move_rqs.append(rq) + + return move_rqs + + @staticmethod + def _execute_move_requests_in_batches(vault, move_rqs): + """Execute move requests in batches respecting the maximum records per batch limit.""" + while move_rqs: + record_count = 0 + requests = [] + + while move_rqs: + rq = move_rqs.pop() + record_rq_count = len(rq['move']) + + if (record_count + record_rq_count) > FolderTransformCommand.MAX_RECORDS_PER_BATCH: + if record_count > 0: + move_rqs.append(rq) # Put it back for next batch + else: + requests.append(rq) # Single large request + break + else: + requests.append(rq) + record_count += record_rq_count + + if requests: + vault.keeper_auth.execute_batch(requests) + + @staticmethod + def move_records(vault: vault_online.VaultOnline, folder_map, is_link): + """Move records from source folders to destination folders.""" + move_rqs = [] + record_permissions_cache = {} + + for src_folder_uid, dst_folder_uid in folder_map: + src_folder = vault.vault_data.get_folder(src_folder_uid) + dst_folder = vault.vault_data.get_folder(dst_folder_uid) + + if not src_folder or not dst_folder: + continue + + src_scope = FolderTransformCommand._get_folder_scope(src_folder) + dst_scope = FolderTransformCommand._get_folder_scope(dst_folder) + + # Determine if we need a scope key for encryption + scope_key = None + if dst_scope != src_scope: + scope_key = FolderTransformCommand._get_scope_key(vault, dst_scope) + + src_type = FolderTransformCommand._get_source_type(src_folder) + + # Process records in chunks + folder_move_rqs = FolderTransformCommand._process_record_chunk( + vault, src_folder.records, src_folder, dst_folder, src_scope, dst_scope, + scope_key, src_type, is_link, record_permissions_cache) + + move_rqs.extend(folder_move_rqs) + + # Execute all move requests in batches + FolderTransformCommand._execute_move_requests_in_batches(vault, move_rqs) + + @staticmethod + def _get_folder_scope_for_deletion(folder): + """Get the scope for a folder when organizing for deletion.""" + if folder.folder_type == FolderTransformCommand.FOLDER_TYPES['user_folder']: + return '' + elif folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + return folder.folder_uid + elif folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder_folder']: + return folder.folder_scope_uid + return None + + @staticmethod + def _organize_folders_by_scope(vault, folders_to_remove): + """Organize folders by their scope for deletion.""" + folder_by_scope = {} + + for folder_uid in folders_to_remove: + folder = vault.vault_data.get_folder(folder_uid) + if not folder: + continue + + folder_scope = FolderTransformCommand._get_folder_scope_for_deletion(folder) + if folder_scope is None: + continue + + if folder_scope not in folder_by_scope: + folder_by_scope[folder_scope] = [] + folder_by_scope[folder_scope].append(folder_uid) + + # Separate user folders from shared folders + user_folders = folder_by_scope.pop('', None) + scopes = list(folder_by_scope.values()) + if user_folders: + scopes.append(user_folders) + + return scopes + + @staticmethod + def _filter_folder_roots(vault, folder_chunk): + """Filter out child folders from a chunk, keeping only root folders.""" + folder_roots = set(folder_chunk) + + for folder_uid in folder_chunk: + if folder_uid in folder_roots: + folder = vault.vault_data.get_folder(folder_uid) + if folder: + vault_utils.traverse_folder_tree( + vault.vault_data, folder, + lambda f: folder_roots.difference_update(f.subfolders or []) + ) + + return [x for x in folder_chunk if x in folder_roots] + + @staticmethod + def _create_delete_object_request(folder, vault): + """Create a delete object request for a folder.""" + rq = { + 'delete_resolution': 'unlink', + 'object_uid': folder.folder_uid, + 'object_type': folder.folder_type, + } + + if folder.parent_uid: + parent_folder = vault.vault_data.get_folder(folder.parent_uid) + if parent_folder: + rq['from_uid'] = parent_folder.folder_uid + rq['from_type'] = parent_folder.folder_type + else: + rq['from_type'] = folder.folder_type + + return rq + + @staticmethod + def _execute_pre_delete(vault, delete_objects): + """Execute pre-delete command and return the token.""" + delete_rq = { + 'command': 'pre_delete', + 'objects': delete_objects, + } + + try: + delete_rs = vault.keeper_auth.execute_auth_command(delete_rq) + if 'pre_delete_response' in delete_rs: + pre_delete = delete_rs['pre_delete_response'] + return pre_delete.get('pre_delete_token', '') + except Exception as e: + logging.debug('Error executing pre-delete: %s', e) + + return '' + + @staticmethod + def _execute_delete(vault, token): + """Execute the actual delete command with the token.""" + if not token: + return + + delete_rq = { + 'command': 'delete', + 'pre_delete_token': token + } + + try: + vault.keeper_auth.execute_auth_command(delete_rq) + except Exception as e: + logging.debug('Error executing delete: %s', e) + + @staticmethod + def _delete_folder_chunk(vault, folder_chunk): + """Delete a chunk of folders.""" + # Filter to only include root folders (not children of other folders in the chunk) + root_folders = FolderTransformCommand._filter_folder_roots(vault, folder_chunk) + + # Create delete object requests + delete_objects = [] + for folder_uid in root_folders: + folder = vault.vault_data.get_folder(folder_uid) + if folder: + delete_obj = FolderTransformCommand._create_delete_object_request(folder, vault) + delete_objects.append(delete_obj) + + if not delete_objects: + return + + # Execute pre-delete and get token + token = FolderTransformCommand._execute_pre_delete(vault, delete_objects) + + # Execute actual delete + FolderTransformCommand._execute_delete(vault, token) + + @staticmethod + def delete_source_tree(vault: vault_online.VaultOnline, folders_to_remove): + """Delete source folders organized by scope.""" + scopes = FolderTransformCommand._organize_folders_by_scope(vault, folders_to_remove) + + for folders in scopes: + while folders: + # Process folders in chunks + chunk_size = FolderTransformCommand.MAX_DELETE_CHUNK_SIZE + chunk = folders[-chunk_size:] + folders = folders[:-chunk_size] + + FolderTransformCommand._delete_folder_chunk(vault, chunk) + + @staticmethod + def _create_folder_request_structure(dst_folder_uid, dst_parent_uid, dst_scope_uid): + """Create the basic folder request structure.""" + sf = folder_pb2.FolderRequest() + sf.folderUid = utils.base64_url_decode(dst_folder_uid) + + if dst_scope_uid: + sf.folderType = folder_pb2.shared_folder_folder + if dst_parent_uid != dst_scope_uid: + sf.parentFolderUid = utils.base64_url_decode(dst_parent_uid) + sf.sharedFolderFolderFields.sharedFolderUid = utils.base64_url_decode(dst_scope_uid) + else: + sf.folderType = folder_pb2.user_folder + sf.parentFolderUid = utils.base64_url_decode(dst_parent_uid) + + return sf + + @staticmethod + def _encrypt_folder_data(folder_name, folder_key, scope_key): + """Encrypt folder data and key.""" + folder_data = {'name': folder_name} + encrypted_data = crypto.encrypt_aes_v1(json.dumps(folder_data).encode('utf-8'), folder_key) + encrypted_key = crypto.encrypt_aes_v1(folder_key, scope_key) + return encrypted_data, encrypted_key + + @staticmethod + def create_target_folder(vault: vault_data.VaultData, source_folder_uid, dst_parent_uid, dst_scope_uid, dst_scope_key): + """Create a target folder request for a source folder.""" + src_subfolder = vault.get_folder(source_folder_uid) + if not src_subfolder: + return None + + dst_folder_uid = utils.generate_uid() + sf = FolderTransformCommand._create_folder_request_structure(dst_folder_uid, dst_parent_uid, dst_scope_uid) + + subfolder_key = utils.generate_aes_key() + encrypted_data, encrypted_key = FolderTransformCommand._encrypt_folder_data( + src_subfolder.name, subfolder_key, dst_scope_key) + + sf.folderData = encrypted_data + sf.encryptedFolderKey = encrypted_key + + return sf + + def _resolve_target_folder(self, target, context): + """Resolve the target folder from the target parameter.""" + if target: + return self.resolve_single_folder(target, context).folder_uid + return None + + def _resolve_source_folders(self, folder_names, context): + """Resolve source folders from folder names.""" + if not folder_names: + raise base.CommandError('At least one folder parameter is required. Example: transform-folder folder1_UID -t target_folder') + + if isinstance(folder_names, str): + folder_names = [folder_names] + + source_folder_uids = set() + for folder_name in folder_names: + folder = self.resolve_single_folder(folder_name, context) + if not folder: + raise base.CommandError(f'Folder "{folder_name}" cannot be found') + source_folder_uids.add(folder.folder_uid) + + return source_folder_uids + + def _validate_folder_operations(self, vault, source_folder_uids, target_folder_uid): + """Validate that folder operations are valid.""" + for folder_uid in source_folder_uids: + src_folder = vault.vault_data.get_folder(folder_uid) + if target_folder_uid and src_folder.parent_uid == target_folder_uid: + raise base.CommandError(f'Folder "{src_folder.folder_uid}" is already in the target') + + # Check for parent-child relationships in source folders + current_folder = src_folder + while current_folder and current_folder.parent_uid: + if current_folder.parent_uid in source_folder_uids: + raise base.CommandError( + f'Folder "{current_folder.parent_uid}" is a parent of "{folder_uid}"\n' + f'Move folder "{folder_uid}" first' + ) + current_folder = vault.vault_data.get_folder(current_folder.parent_uid) + + def _determine_target_folder_type(self, source_folder, target_folder_uid, kwargs): + """Determine the target folder type based on source and parameters.""" + if target_folder_uid is None: + if source_folder.parent_uid: + is_target_shared = kwargs.get('folder_type') in ['shared', 'shared_folder'] + else: + is_target_shared = source_folder.folder_type == FolderTransformCommand.FOLDER_TYPES['user_folder'] + return 'shared_folder' if is_target_shared else 'user_folder' + return None + + def _create_root_folder_request(self, source_folder, target_folder_uid, target_scope_uid, + target_scope_key, target_key, folder_key, kwargs): + """Create a folder request for the root folder.""" + target_uid = utils.generate_uid() + f = folder_pb2.FolderRequest() + f.folderUid = utils.base64_url_decode(target_uid) + + data = {'name': source_folder.name} + f.folderData = crypto.encrypt_aes_v1(json.dumps(data).encode('utf-8'), folder_key) + + if target_folder_uid is None: + folder_type = self._determine_target_folder_type(source_folder, target_folder_uid, kwargs) + if folder_type == 'shared_folder': + f.folderType = 'shared_folder' + f.sharedFolderFields.encryptedFolderName = crypto.encrypt_aes_v1(source_folder.name.encode(), folder_key) + else: + f.folderType = 'user_folder' + else: + # This will be handled in the calling method where we have access to vault + return None, None + + f.encryptedFolderKey = crypto.encrypt_aes_v1(folder_key, target_key) + return f, target_uid + + def _count_folder_contents(self, vault_data, source_folder, src_to_dst_map, target_scope_uid, target_scope_key, folders_to_create, folders_to_remove): + """Count subfolders and records in a folder tree.""" + subfolder_count = 0 + record_count = 0 + + def add_subfolders(folder: vault_types.Folder): + nonlocal subfolder_count, record_count + subfolder_count += 1 + records = folder.records + if isinstance(records, set): + record_count += len(records) + + dst_folder_uid = src_to_dst_map.get(folder.folder_uid) + if dst_folder_uid: + for src_subfolder_uid in folder.subfolders: + folder_rq = self.create_target_folder( + vault_data, src_subfolder_uid, dst_folder_uid, target_scope_uid, target_scope_key) + if folder_rq: + dst_subfolder_uid = utils.base64_url_encode(folder_rq.folderUid) + folders_to_create.append(folder_rq) + folders_to_remove.append(src_subfolder_uid) + src_to_dst_map[src_subfolder_uid] = dst_subfolder_uid + + vault_utils.traverse_folder_tree(vault_data, source_folder, add_subfolders) + return subfolder_count, record_count + + def _create_folder_structure(self, vault, source_folder_uids, target_folder_uid, kwargs): + """Create the folder structure for transformation.""" + folders_to_remove = [] + folders_to_create = [] + src_to_dst_map = {} + table = [] + + for source_uid in source_folder_uids: + source_folder = vault.vault_data.get_folder(source_uid) + if not source_folder: + continue + + target_scope_uid = '' + target_scope_key = vault.keeper_auth.auth_context.data_key + target_key = vault.keeper_auth.auth_context.data_key + folder_key = utils.generate_aes_key() + + # Create root folder request + if target_folder_uid is None: + folder_request, target_uid = self._create_root_folder_request( + source_folder, target_folder_uid, target_scope_uid, target_scope_key, target_key, folder_key, kwargs) + + if folder_request is None: + continue + + # Update scope information for shared folders + if folder_request.folderType == 'shared_folder': + target_scope_uid = target_uid + target_scope_key = folder_key + else: + # Handle target folder case + target_folder = vault.vault_data.get_folder(target_folder_uid) + if not target_folder: + continue + + target_uid = utils.generate_uid() + folder_request = folder_pb2.FolderRequest() + folder_request.folderUid = utils.base64_url_decode(target_uid) + + data = {'name': source_folder.name} + folder_request.folderData = crypto.encrypt_aes_v1(json.dumps(data).encode('utf-8'), folder_key) + + if target_folder.folder_type == FolderTransformCommand.FOLDER_TYPES['user_folder']: + folder_request.folderType = 'user_folder' + folder_request.parentFolderUid = utils.base64_url_decode(target_folder.folder_uid) + elif target_folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder']: + folder_request.folderType = 'shared_folder_folder' + target_scope_uid = target_folder.folder_uid + target_scope_key = vault.vault_data.get_shared_folder_key(target_folder.folder_uid) + target_key = target_scope_key + folder_request.sharedFolderFolderFields.sharedFolderUid = utils.base64_url_decode(target_scope_uid) + elif target_folder.folder_type == FolderTransformCommand.FOLDER_TYPES['shared_folder_folder']: + folder_request.folderType = 'shared_folder_folder' + target_scope_uid = target_folder.folder_scope_uid + target_scope_key = vault.vault_data.get_shared_folder_key(target_scope_uid) + target_key = target_scope_key + folder_request.sharedFolderFolderFields.sharedFolderUid = utils.base64_url_decode(target_scope_uid) + folder_request.parentFolderUid = utils.base64_url_decode(target_folder.folder_uid) + else: + continue + + folder_request.encryptedFolderKey = crypto.encrypt_aes_v1(folder_key, target_key) + + folders_to_create.append(folder_request) + folders_to_remove.append(source_uid) + src_to_dst_map[source_uid] = target_uid + + # Count contents and create subfolder requests + subfolder_count, record_count = self._count_folder_contents( + vault.vault_data, source_folder, src_to_dst_map, target_scope_uid, target_scope_key, folders_to_create, folders_to_remove) + + folder_path = vault_utils.get_folder_path(vault.vault_data, source_uid) + table.append([folder_path, subfolder_count, record_count]) + + return folders_to_remove, folders_to_create, src_to_dst_map, table + + def _display_transformation_preview(self, table, is_link, target_folder_uid, vault_data): + """Display the transformation preview to the user.""" + headers = ['Source Folder', 'Folder Count', 'Record Count'] + operation = 'copied' if is_link else 'moved' + target_name = vault_utils.get_folder_path(vault_data, target_folder_uid) if target_folder_uid else 'My Vault' + title = f'The following folders will be {operation} to "{target_name}"' + report_utils.dump_report_data(table, headers=headers, title=title) + + def _confirm_transformation(self, kwargs): + """Get user confirmation for the transformation.""" + if kwargs.get('force') is not True: + inp = prompt_utils.user_choice( + 'Are you sure you want to proceed with this action?', + FolderTransformCommand.CONFIRMATION_CHOICES, + default=FolderTransformCommand.DEFAULT_CONFIRMATION) + if inp.lower() == 'y': + logging.info('Executing transformation(s)...') + return True + else: + logging.info('Cancelled.') + return False + return True + + def _create_folders_in_batches(self, vault, folders_to_create): + """Create folders in batches.""" + while folders_to_create: + chunk = folders_to_create[:FolderTransformCommand.MAX_FOLDERS_PER_CHUNK] + folders_to_create = folders_to_create[FolderTransformCommand.MAX_FOLDERS_PER_CHUNK:] + + rq = folder_pb2.ImportFolderRecordRequest() + for folder_request in chunk: + rq.folderRequest.append(folder_request) + + rs = vault.keeper_auth.execute_auth_rest( + request=rq, + rest_endpoint='folder/import_folders_and_records', + response_type=folder_pb2.ImportFolderRecordResponse) + + errors = [x for x in rs.folderResponse if x.status.upper() != 'SUCCESS'] + if errors: + raise base.CommandError(f'Failed to re-create folder structure: {errors[0].status}') + + def _execute_transformation_steps(self, vault, source_folder_uids, src_to_dst_map, + folders_to_remove, folders_to_create, is_link): + """Execute the transformation steps.""" + # Create folders + self._create_folders_in_batches(vault, folders_to_create) + vault.sync_down() + + # Rename source folders (if not linking) + if not is_link: + self.rename_source_folders(vault, source_folder_uids) + vault.sync_down() + + # Move records + self.move_records(vault, src_to_dst_map.items(), is_link) + vault.sync_down() + + # Delete source tree (if not linking) + if not is_link: + self.delete_source_tree(vault, folders_to_remove) + + vault.sync_down() + + def execute(self, context: KeeperParams, **kwargs): + """Execute the folder transformation command.""" + assert context.vault is not None + vault = context.vault + + # Resolve target and source folders + target_folder_uid = self._resolve_target_folder(kwargs.get('target'), context) + source_folder_uids = self._resolve_source_folders(kwargs.get('folder'), context) + + # Validate operations + self._validate_folder_operations(vault, source_folder_uids, target_folder_uid) + + is_link = kwargs.get('link') is True + + # Create folder structure + folders_to_remove, folders_to_create, src_to_dst_map, table = self._create_folder_structure( + vault, source_folder_uids, target_folder_uid, kwargs) + + # Display preview and get confirmation + self._display_transformation_preview(table, is_link, target_folder_uid, vault.vault_data) + + if kwargs.get('dry_run') is True: + return + + if not self._confirm_transformation(kwargs): + return + + # Execute transformation + self._execute_transformation_steps(vault, source_folder_uids, src_to_dst_map, + folders_to_remove, folders_to_create, is_link) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/helpers/share_utils.py b/keepercli-package/src/keepercli/helpers/share_utils.py index 198a729f..3eb2c2ca 100644 --- a/keepercli-package/src/keepercli/helpers/share_utils.py +++ b/keepercli-package/src/keepercli/helpers/share_utils.py @@ -1,19 +1,65 @@ import datetime import itertools -from typing import Optional, Dict, List, Any, Generator, Tuple, Iterable +from typing import Optional, Dict, List, Any, Generator, Iterable, Set from keepersdk import crypto, utils -from keepersdk.proto import record_pb2 -from keepersdk.vault import storage_types, vault_online, vault_record +from keepersdk.proto import enterprise_pb2, record_pb2 +from keepersdk.vault import storage_types, vault_online, vault_record, vault_utils from .. import api from ..commands import enterprise_utils -from ..helpers import timeout_utils +from ..helpers import timeout_utils, folder_utils from ..params import KeeperParams - +# Constants RECORD_DETAILS_URL = 'vault/get_records_details' SHARE_OBJECTS_API = 'vault/get_share_objects' +TEAM_MEMBERS_ENDPOINT = 'vault/get_team_members' +SHARING_ADMINS_ENDPOINT = 'enterprise/get_sharing_admins' + +# Record processing constants +CHUNK_SIZE = 999 +RECORD_KEY_LENGTH_V2 = 60 +DEFAULT_EXPIRATION = 0 +NEVER_EXPIRES = -1 +NEVER_EXPIRES_STRING = 'never' + +# Record version constants +MAX_V2_VERSION = 2 +V3_VERSION = 3 +V4_VERSION = 4 + +# User type constants +TEAM_USER_TYPE = 2 + +# Permission field names +CAN_SHARE_PERMISSION = 'can_share' +CAN_EDIT_FIELD = 'can_edit' +CAN_SHARE_FIELD = 'can_share' +CAN_VIEW_FIELD = 'can_view' +RECORD_UID_FIELD = 'record_uid' +SHARED_FOLDER_UID_FIELD = 'shared_folder_uid' +TEAM_UID_FIELD = 'team_uid' + +# Share object categories +RELATIONSHIP_CATEGORY = 'relationship' +FAMILY_CATEGORY = 'family' +ENTERPRISE_CATEGORY = 'enterprise' +MC_CATEGORY = 'mc' + +# Default empty dictionaries +EMPTY_SHARE_OBJECTS = {'users': {}, 'enterprises': {}, 'teams': {}} + +# Record field names +TITLE_FIELD = 'title' +NAME_FIELD = 'name' +IS_SA_FIELD = 'is_sa' +ENTERPRISE_ID_FIELD = 'enterprise_id' +STATUS_FIELD = 'status' +CATEGORY_FIELD = 'category' +SHARES_FIELD = 'shares' +USER_PERMISSIONS_FIELD = 'user_permissions' +SHARED_FOLDER_PERMISSIONS_FIELD = 'shared_folder_permissions' logger = api.get_logger() @@ -21,16 +67,16 @@ def get_share_expiration(expire_at: Optional[str], expire_in: Optional[str]) -> int: if not expire_at and not expire_in: - return 0 + return DEFAULT_EXPIRATION dt = None if isinstance(expire_at, str): - if expire_at == 'never': - return -1 + if expire_at == NEVER_EXPIRES_STRING: + return NEVER_EXPIRES dt = datetime.datetime.fromisoformat(expire_at) elif isinstance(expire_in, str): - if expire_in == 'never': - return -1 + if expire_in == NEVER_EXPIRES_STRING: + return NEVER_EXPIRES td = timeout_utils.parse_timeout(expire_in) dt = datetime.datetime.now() + td if dt is None: @@ -49,24 +95,24 @@ def get_share_objects(vault: vault_online.VaultOnline) -> Dict[str, Dict[str, An ) if not response: - return {'users': {}, 'enterprises': {}, 'teams': {}} + return EMPTY_SHARE_OBJECTS users_by_type = { - 'relationship': response.shareRelationships, - 'family': response.shareFamilyUsers, - 'enterprise': response.shareEnterpriseUsers, - 'mc': response.shareMCEnterpriseUsers, + RELATIONSHIP_CATEGORY: response.shareRelationships, + FAMILY_CATEGORY: response.shareFamilyUsers, + ENTERPRISE_CATEGORY: response.shareEnterpriseUsers, + MC_CATEGORY: response.shareMCEnterpriseUsers, } def process_users(users_data: Iterable[Any], category: str) -> Dict[str, Dict[str, Any]]: """Process user data and add category information.""" return { user.username: { - 'name': user.fullname, - 'is_sa': user.isShareAdmin, - 'enterprise_id': user.enterpriseId, - 'status': user.status, - 'category': category + NAME_FIELD: user.fullname, + IS_SA_FIELD: user.isShareAdmin, + ENTERPRISE_ID_FIELD: user.enterpriseId, + STATUS_FIELD: user.status, + CATEGORY_FIELD: category } for user in users_data } @@ -82,8 +128,8 @@ def process_users(users_data: Iterable[Any], category: str) -> Dict[str, Dict[st def process_teams(teams_data: Iterable[Any]) -> Dict[str, Dict[str, Any]]: return { utils.base64_url_encode(team.teamUid): { - 'name': team.teamname, - 'enterprise_id': team.enterpriseId + NAME_FIELD: team.teamname, + ENTERPRISE_ID_FIELD: team.enterpriseId } for team in teams_data } @@ -122,7 +168,7 @@ def load_records_in_shared_folder( if isinstance(getattr(rk, 'record_key', b''), bytes) else getattr(rk, 'record_key', '') ) - if len(key) == 60: + if len(key) == RECORD_KEY_LENGTH_V2: record_key = crypto.decrypt_aes_v2(key, shared_folder_key) else: record_key = crypto.decrypt_aes_v1(key, shared_folder_key) @@ -186,19 +232,19 @@ def load_records_in_shared_folder( 'client_modified_time': record_data.clientModifiedTime, } data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) - if version <= 2: + if version <= MAX_V2_VERSION: record['data_unencrypted'] = crypto.decrypt_aes_v1(data_decoded, record_key) else: record['data_unencrypted'] = crypto.decrypt_aes_v2(data_decoded, record_key) # Handle extra data for v2 records - if record_data.encryptedExtraData and version <= 2: + if record_data.encryptedExtraData and version <= MAX_V2_VERSION: record['extra'] = record_data.encryptedExtraData extra_decoded = utils.base64_url_decode(record_data.encryptedExtraData) record['extra_unencrypted'] = crypto.decrypt_aes_v1(extra_decoded, record_key) # Handle v3 typed records with references - if version == 3: + if version == V3_VERSION: v3_record = vault.vault_data.load_record(record_uid=record_uid) if isinstance(v3_record, vault_record.TypedRecord): for ref in itertools.chain(v3_record.fields, v3_record.custom): @@ -206,7 +252,7 @@ def load_records_in_shared_folder( record_set.update(ref.value) # Handle v4 records with file attachments - elif version == 4: + elif version == V4_VERSION: if record_data.fileSize > 0: record['file_size'] = record_data.fileSize if record_data.thumbnailSize > 0: @@ -257,11 +303,11 @@ def needs_share_info(uid: str) -> bool: def create_record_info(record_uid: str, keeper_record: Optional[Any] = None) -> Dict[str, Any]: """Create basic record information dictionary.""" - rec = {'record_uid': record_uid} + rec = {RECORD_UID_FIELD: record_uid} if keeper_record: - if hasattr(keeper_record, 'title'): - rec['title'] = keeper_record.title + if hasattr(keeper_record, TITLE_FIELD): + rec[TITLE_FIELD] = keeper_record.title if hasattr(keeper_record, 'data_unencrypted'): rec['data_unencrypted'] = keeper_record.data_unencrypted @@ -307,7 +353,7 @@ def process_shared_folder_permissions(info: Any) -> List[Dict[str, Any]]: result = [] try: - chunk_size = 999 + chunk_size = CHUNK_SIZE for i in range(0, len(uids_needing_info), chunk_size): chunk = uids_needing_info[i:i + chunk_size] @@ -346,7 +392,7 @@ def process_shared_folder_permissions(info: Any) -> List[Dict[str, Any]]: def resolve_record_share_path(context: KeeperParams, record_uid: str) -> Optional[Dict[str, str]]: - return resolve_record_permission_path(context=context, record_uid=record_uid, permission='can_share') + return resolve_record_permission_path(context=context, record_uid=record_uid, permission=CAN_SHARE_PERMISSION) def resolve_record_permission_path( @@ -357,12 +403,12 @@ def resolve_record_permission_path( for ap in enumerate_record_access_paths(context=context, record_uid=record_uid): if ap.get(permission): path = { - 'record_uid': record_uid + RECORD_UID_FIELD: record_uid } - if 'shared_folder_uid' in ap: - path['shared_folder_uid'] = ap['shared_folder_uid'] - if 'team_uid' in ap: - path['team_uid'] = ap['team_uid'] + if SHARED_FOLDER_UID_FIELD in ap: + path[SHARED_FOLDER_UID_FIELD] = ap[SHARED_FOLDER_UID_FIELD] + if TEAM_UID_FIELD in ap: + path[TEAM_UID_FIELD] = ap[TEAM_UID_FIELD] return path return None @@ -381,14 +427,14 @@ def create_access_path( ) -> Dict[str, Any]: """Create a standardized access path dictionary.""" path = { - 'record_uid': record_uid, - 'shared_folder_uid': shared_folder_uid, - 'can_edit': can_edit, - 'can_share': can_share, - 'can_view': True + RECORD_UID_FIELD: record_uid, + SHARED_FOLDER_UID_FIELD: shared_folder_uid, + CAN_EDIT_FIELD: can_edit, + CAN_SHARE_FIELD: can_share, + CAN_VIEW_FIELD: True } if team_uid: - path['team_uid'] = team_uid + path[TEAM_UID_FIELD] = team_uid return path def process_team_permissions( @@ -401,7 +447,7 @@ def process_team_permissions( return for user_permission in shared_folder.user_permissions: - if user_permission.user_type != storage_types.SharedFolderUserType.Team: + if user_permission.user_type != TEAM_USER_TYPE: continue team_uid = user_permission.user_uid @@ -437,3 +483,245 @@ def process_team_permissions( else: yield from process_team_permissions(shared_folder, can_edit, can_share) + +def get_shared_records(context: KeeperParams, record_uids, cache_only=False): + """ + Get shared record information for the specified record UIDs. + + Args: + context: KeeperParams instance containing vault and enterprise data + record_uids: Collection of record UIDs to process + cache_only: If True, only use cached data without making API calls + + Returns: + Dict mapping record UIDs to SharedRecord instances + """ + + def _fetch_team_members_from_api(team_uids: Set[str]) -> Dict[str, Set[str]]: + """Fetch team members from the API for the given team UIDs.""" + members = {} + + if not context.vault.keeper_auth.auth_context.enterprise_ec_public_key: + return members + + for team_uid in team_uids: + try: + request = enterprise_pb2.GetTeamMemberRequest() + request.teamUid = utils.base64_url_decode(team_uid) + + response = context.vault.keeper_auth.execute_auth_rest( + rest_endpoint=TEAM_MEMBERS_ENDPOINT, + request=request, + response_type=enterprise_pb2.GetTeamMemberResponse + ) + + if response and response.enterpriseUser: + team_members = {user.email for user in response.enterpriseUser} + members[team_uid] = team_members + + except Exception as e: + logger.debug(f"Failed to fetch team members for {team_uid}: {e}") + + return members + + def _get_cached_team_members(team_uids: Set[str], username_lookup: Dict[str, str]) -> Dict[str, Set[str]]: + """Get team members from cached enterprise data.""" + members = {} + + if not context.enterprise_data: + return members + + team_user_links = context.enterprise_data.team_users.get_all_links() or [] + + relevant_team_users = [ + link for link in team_user_links + if link.user_type != 2 and link.team_uid in team_uids + ] + + for team_user in relevant_team_users: + username = username_lookup.get(team_user.enterprise_user_id) + if username: + team_uid = team_user.team_uid + if team_uid not in members: + members[team_uid] = set() + members[team_uid].add(username) + + return members + + def _fetch_shared_folder_admins() -> Dict[str, List[str]]: + """Fetch share administrators for all shared folders.""" + sf_uids = list(context.vault.vault_data._shared_folders.keys()) + return { + sf_uid: get_share_admins_for_shared_folder(context.vault, sf_uid) or [] + for sf_uid in sf_uids + } + + def _get_restricted_role_members(username_lookup: Dict[str, str]) -> Set[str]: + """Get usernames with restricted sharing permissions.""" + if not context.enterprise_data: + return set() + + role_enforcements = context.enterprise_data.role_enforcements.get_all_links() + restricted_roles = { + re.role_id for re in role_enforcements + if re.enforcement_type == 'enforcements' and re.value == 'restrict_sharing_all' + } + + if not restricted_roles: + return set() + + restricted_users = context.enterprise_data.role_users.get_links_by_object(restricted_roles) + restricted_teams = context.enterprise_data.role_teams.get_links_by_object(restricted_roles) + + restricted_members = set() + + for user_link in restricted_users: + username = username_lookup.get(user_link.enterprise_user_id) + if username: + restricted_members.add(username) + + team_uids = {team_link.team_uid for team_link in restricted_teams} + if team_uids: + team_members = _get_cached_team_members(team_uids, username_lookup) + for members in team_members.values(): + restricted_members.update(members) + + return restricted_members + + try: + shares = get_record_shares(context.vault, record_uids) + + sf_teams = [share.get('teams', []) for share in shares] if shares else [] + team_uids = { + team.get('team_uid') + for teams in sf_teams + for team in teams + if team.get('team_uid') + } + + enterprise_users = context.enterprise_data.users.get_all_entities() if context.enterprise_data else [] + username_lookup = {user.enterprise_user_id: user.username for user in enterprise_users} + + sf_share_admins = _fetch_shared_folder_admins() if not cache_only else {} + + restricted_role_members = _get_restricted_role_members(username_lookup) + + if cache_only or context.enterprise_data: + team_members = _get_cached_team_members(team_uids, username_lookup) + else: + team_members = _fetch_team_members_from_api(team_uids) + + records = [context.vault.vault_data.get_record(uid) for uid in record_uids] + valid_records = [record for record in records if record is not None] + + shared_records = [ + SharedRecord(record, sf_share_admins, team_members, restricted_role_members) + for record in valid_records + ] + + return {shared_record.record_uid: shared_record for shared_record in shared_records} + + except Exception as e: + logger.error(f"Error in get_shared_records: {e}") + return {} + + +def get_share_admins_for_shared_folder(vault: vault_online.VaultOnline, shared_folder_uid): + if vault.keeper_auth.auth_context.enterprise_ec_public_key: + try: + rq = enterprise_pb2.GetSharingAdminsRequest() + rq.sharedFolderUid = utils.base64_url_decode(shared_folder_uid) + rs = vault.keeper_auth.execute_auth_rest( + rest_endpoint=SHARING_ADMINS_ENDPOINT, + request=rq, + response_type=enterprise_pb2.GetSharingAdminsResponse + ) + admins = [x.email for x in rs.userProfileExts if x.isShareAdminForSharedFolderOwner and x.isInSharedFolder] + except Exception as e: + logger.debug(e) + return + return admins + + +def get_folder_uids(context: KeeperParams, name: str) -> set[str]: + """Get folder UIDs by name or path.""" + folder_uids = set() + + if not context.vault or not context.vault.vault_data: + return folder_uids + + if name in context.vault.vault_data._folders: + folder_uids.add(name) + return folder_uids + + for folder in context.vault.vault_data.folders(): + if folder.name == name: + folder_uids.add(folder.folder_uid) + + if not folder_uids: + try: + folder, _ = folder_utils.try_resolve_path(context, name) + if folder: + folder_uids.add(folder.folder_uid) + except: + pass + + return folder_uids + + +def get_contained_record_uids(vault: vault_online.VaultOnline, name: str, children_only: bool = True) -> Dict[str, Set[str]]: + records_by_folder = dict() + root_folder_uids = get_folder_uids(vault, name) + + def add_child_recs(f_uid): + folder = vault.vault_data.get_folder(f_uid) + child_recs = folder.records + records_by_folder.update({f_uid: child_recs}) + + def on_folder(f): + f_uid = f.folder_uid or '' + if not children_only or f_uid in root_folder_uids: + add_child_recs(f_uid) + + for uid in root_folder_uids: + folder = vault.vault_data.get_folder(uid) + vault_utils.traverse_folder_tree(vault.vault_data, folder, on_folder) + + return records_by_folder + + +class SharedRecord: + + def __init__(self, record, sf_share_admins, team_members, restricted_role_members): + self.record = record + self.sf_share_admins = sf_share_admins or {} + self.team_members = team_members or {} + self.restricted_role_members = restricted_role_members or set() + + @property + def record_uid(self) -> str: + return self.record.record_uid + + @property + def title(self) -> str: + return getattr(self.record, 'title', '') + + def get_all_share_admins(self) -> List[str]: + admin_usernames = [] + for sf_uid, admins_list in self.sf_share_admins.items(): + if admins_list: + admin_usernames.extend(admins_list) + return list(set(admin_usernames)) # Remove duplicates + + def get_share_admins_for_folder(self, sf_uid: str) -> List[str]: + return self.sf_share_admins.get(sf_uid, []) + + def get_all_team_members(self) -> Set[str]: + all_members = set() + for team_uid, members in self.team_members.items(): + if members: + all_members.update(members) + return all_members + + def is_restricted_user(self, username: str) -> bool: + return username in self.restricted_role_members \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 91f814f0..1583d3aa 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -27,7 +27,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, record_type, secrets_manager, share_management, password_report, trash, record_file_report, - record_handling_commands) + record_handling_commands, register) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -37,6 +37,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('rmdir', vault_folder.FolderRemoveCommand(), base.CommandScope.Vault) commands.register_command('rndir', vault_folder.FolderRenameCommand(), base.CommandScope.Vault) commands.register_command('mv', vault_folder.FolderMoveCommand(), base.CommandScope.Vault) + commands.register_command('transform-folder', vault_folder.FolderTransformCommand(), base.CommandScope.Vault) commands.register_command('list', vault_record.RecordListCommand(), base.CommandScope.Vault, 'l') commands.register_command('list-sf', vault_record.SharedFolderListCommand(), base.CommandScope.Vault, 'lsf') commands.register_command('list-team', vault_record.TeamListCommand(), base.CommandScope.Vault, 'lt') @@ -45,6 +46,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('record-history', record_handling_commands.RecordHistoryCommand(), base.CommandScope.Vault, 'rh') commands.register_command('clipboard-copy', record_handling_commands.ClipboardCommand(), base.CommandScope.Vault, 'cc') commands.register_command('find-password', record_handling_commands.ClipboardCommand(), base.CommandScope.Vault) + commands.register_command('find-ownerless', register.FindOwnerlessCommand(), base.CommandScope.Vault) commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') commands.register_command('record-update', record_edit.RecordUpdateCommand(), base.CommandScope.Vault, 'ru') commands.register_command('rm', record_edit.RecordDeleteCommand(), base.CommandScope.Vault) From 1e1743296816d97831bfdf03f798bf0ddc27a42a Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 16 Oct 2025 18:14:45 +0530 Subject: [PATCH 35/44] Readme update in detail --- README.md | 340 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 311 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index ac29dd2a..676217af 100644 --- a/README.md +++ b/README.md @@ -2,69 +2,351 @@ [![License](https://img.shields.io/pypi/l/keepersdk)](https://github.com/Keeper-Security/keeper-sdk-python/blob/master/LICENSE) ![Python](https://img.shields.io/pypi/pyversions/keepersdk) ![License](https://img.shields.io/pypi/status/keepersdk) -# Keeper-Security/keeper-sdk-python -### Keeper SDK for Python -### Installation +# Keeper SDK for Python + +## Overview + +The Keeper SDK for Python provides developers with a comprehensive toolkit for integrating Keeper Security's password management and secrets management capabilities into Python applications. This repository contains two primary packages: + +- **Keeper SDK (`keepersdk`)**: A Python library for programmatic access to Keeper Vault, enabling developers to build custom integrations, automate password management workflows, and manage enterprise console operations. +- **Keeper CLI (`keepercli`)**: A modern command-line interface for interacting with Keeper Vault and Enterprise Console, offering efficient commands for vault management, enterprise administration, and automation tasks. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Keeper SDK](#keeper-sdk) + - [SDK Installation](#sdk-installation) + - [SDK Environment Setup](#sdk-environment-setup) + - [SDK Configuration](#sdk-configuration) + - [SDK Usage Example](#sdk-usage-example) +- [Keeper CLI](#keeper-cli) + - [CLI Installation](#cli-installation) + - [CLI Environment Setup](#cli-environment-setup) + - [CLI Usage](#cli-usage) +- [Development Setup](#development-setup) +- [Contributing](#contributing) +- [License](#license) + +--- + +## Prerequisites + +Before installing the Keeper SDK or CLI, ensure your system meets the following requirements: + +- **Python Version**: Python 3.10 or higher +- **Operating System**: Windows, macOS, or Linux +- **Package Manager**: pip (Python package installer) +- **Virtual Environment** (recommended): `venv` or `virtualenv` + +To verify your Python version: +```bash +python3 --version +``` + +--- + +## Keeper SDK + +### About Keeper SDK + +The Keeper SDK is a Python library that provides programmatic access to Keeper Security's platform. It enables developers to: + +- Authenticate users and manage sessions +- Access and manipulate vault records (passwords, files, custom fields) +- Manage folders and shared folders +- Administer enterprise console operations (users, teams, roles, nodes) +- Integrate Keeper's zero-knowledge security architecture into applications +- Automate password rotation and secrets management workflows + +### SDK Installation + +#### From PyPI (Recommended) + +Install the latest stable release from the Python Package Index: + ```bash pip install keepersdk ``` -### Clone source code +#### From Source + +To install from source for development or testing purposes: + ```bash -$ git clone https://github.com/Keeper-Security/keeper-sdk-python +# Clone the repository +git clone https://github.com/Keeper-Security/keeper-sdk-python +cd keeper-sdk-python/keepersdk-package + +# Install dependencies +pip install -r requirements.txt + +# Install the SDK +pip install . ``` -### Steps to setup environment for using python keepersdk +### SDK Environment Setup + +For optimal development practices, it's recommended to use a virtual environment: + +**Step 1: Create a Virtual Environment** + +```bash +# On macOS/Linux +python3 -m venv venv + +# On Windows +python -m venv venv ``` -Requirement - python 3.10 or higher -1. Open a terminal/zsh/powershell -2. Create a virtual environment (venv) using "python3 -m venv venv" (Optionally python or py depending on python setup) -3. Activate the venv using "source venv/bin/activate" for MacOS/Linux or "venv\Scripts\Activate' for Windows -4. Move to keepersdk-package using "cd keepersdk-package" -5. Run "pip install -r requirements.txt" for installing dependencies -6. Run "pip install setuptools" tp install setuptools which will be used to create keepersdk package -7. Run "python setup.py install" to install keepersdk from the keepersdk-package as a lib -8. Create a client file for the keepersdk to use it complete the login flow and access Keeper Vault and Console elements. An example is added below. + +**Step 2: Activate the Virtual Environment** + +```bash +# On macOS/Linux +source venv/bin/activate + +# On Windows +venv\Scripts\activate ``` +**Step 3: Install Keeper SDK dependencies** -### Steps to setup enviroment for using python keepercli-package +```bash +pip install -r requirements.txt +pip install setuptools ``` -Continuing from step 7 of previous section to setup keepersdk-package -8. Move to keepercli-package using "cd ../keepercli-package" or "cd ..\keepercli-package" -9. Run "pip install -r requirements.txt" for installing dependencies -10. Run "python setup.py install" to install keepercli from the keepercli-package as a lib -11. Run command "python -m keepercli" to run the keepercli which is the new version of Commander CLI with more efficient commands + +**Step 4: Install keepersdk into the venv** +```bash +python setup.py install ``` -### Example +Your environment is now ready for SDK development. + +### SDK Configuration + +The Keeper SDK uses a configuration storage system to manage authentication settings and endpoints. You can use: + +- **JsonConfigurationStorage**: Stores configuration in JSON format (default) +- **InMemoryConfigurationStorage**: Temporary in-memory storage for testing +- **Custom implementations**: Implement your own configuration storage + +No additional configuration files are required for basic usage. Authentication settings are managed programmatically through the SDK. + +### SDK Usage Example + +Below is a complete example demonstrating authentication, vault synchronization, and record retrieval: ```python import sqlite3 - from keepersdk.authentication import login_auth, configuration, endpoint from keepersdk.vault import sqlite_storage, vault_online, vault_record +# Initialize configuration and authentication context config = configuration.JsonConfigurationStorage() keeper_endpoint = endpoint.KeeperEndpoint(config) login_auth_context = login_auth.LoginAuth(keeper_endpoint) + +# Authenticate user login_auth_context.login('username@company.com') -# bypassing device approval and 2fa step, not recommended -login_auth_context.login_step.verify_password('yourpassword') +# Complete password verification +# Note: In case 2fa and device approval flows fail, we can use verify password +# login_auth_context.login_step.verify_password('your_secure_password') + +while not login_auth_context.login_step.is_final(): + if isinstance(login_auth_context.login_step, login_auth.LoginStepDeviceApproval): + login_auth_context.login_step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): + password = PromptSession().prompt('Enter password: ', is_password=True) + login_auth_context.login_step.verify_password(password) + elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): + channel = login_auth_context.login_step.get_channels()[0] + code = PromptSession().prompt(f'Enter 2FA code for {channel.channel_name}: ', is_password=True) + login_auth_context.login_step.send_code(channel.channel_uid, code) + else: + raise NotImplementedError() + +# Check if login was successful if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + # Obtain authenticated session keeper_auth = login_auth_context.login_step.take_keeper_auth() + + # Set up vault storage (using SQLite in-memory database) conn = sqlite3.Connection('file::memory:', uri=True) - vault_storage = sqlite_storage.SqliteVaultStorage(lambda: conn, vault_owner=bytes(keeper_auth.auth_context.username, 'utf-8')) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth.auth_context.username, 'utf-8') + ) + + # Initialize vault and synchronize with Keeper servers vault = vault_online.VaultOnline(keeper_auth, vault_storage) vault.sync_down() - - # List records + + # Access and display vault records + print("Vault Records:") + print("-" * 50) for record in vault.vault_data.records(): print(f'Title: {record.title}') + + # Handle legacy (v2) records if record.version == 2: legacy_record = vault.vault_data.load_record(record.record_uid) if isinstance(legacy_record, vault_record.PasswordRecord): print(f'Username: {legacy_record.login}') -``` \ No newline at end of file + print(f'URL: {legacy_record.link}') + + # Handle modern (v3+) records + elif record.version >= 3: + print(f'Record Type: {record.record_type}') + + print("-" * 50) +``` + +**Important Security Notes:** +- Never hardcode credentials in production code +- Always implement proper two-factor authentication +- Use device approval flows for enhanced security +- Consider using environment variables or secure vaults for credential management + +--- + +## Keeper CLI + +### About Keeper CLI + +Keeper CLI is a powerful command-line interface that provides direct access to Keeper Vault and Enterprise Console features. It enables users to: + +- Manage vault records, folders, and attachments from the terminal +- Perform enterprise administration tasks (user management, team operations, role assignments) +- Execute batch operations and automation scripts +- Generate audit reports and monitor security events +- Configure Secrets Manager applications +- Import and export vault data + +Keeper CLI is ideal for system administrators, DevOps engineers, and power users who prefer terminal-based workflows. + +### CLI Installation + +#### From Source + +```bash +# Clone the repository +git clone https://github.com/Keeper-Security/keeper-sdk-python +cd keeper-sdk-python/keepercli-package + +# Install dependencies +pip install -r requirements.txt +python setup.py install +``` + +### CLI Environment Setup + +**Complete Setup from Source:** + +**Step 1: Create and Activate Virtual Environment** + +```bash +# Create virtual environment +python3 -m venv venv + +# Activate virtual environment +# On macOS/Linux: +source venv/bin/activate +# On Windows: +venv\Scripts\activate +``` + +**Step 2: Install Keeper SDK (Required Dependency)** + +```bash +cd keepersdk-package +pip install -r requirements.txt +pip install setuptools +python setup.py install +``` + +**Step 3: Install Keeper CLI** + +```bash +cd ../keepercli-package +pip install -r requirements.txt +``` + +### CLI Usage + +Once installed, launch Keeper CLI: + +```bash +# Run Keeper CLI +python -m keepercli +``` + +**Common CLI Commands:** + +```bash +# Login to your Keeper account +Not Logged In> login + +# List all vault records +My Vault> list + +# Search for a specific record +My Vault> search + +# Display record details +My Vault> get + +# Add a new record +My Vault> add-record + +# Sync vault with server +My Vault> sync-down + +# Enterprise user management +My Vault> enterprise-user list +My Vault> enterprise-user add +My Vault> enterprise-user edit + +# Team management +My Vault> enterprise-team list +My Vault> enterprise-team add + +# Generate audit report +My Vault> audit-report + +# Exit CLI +My Vault> quit +``` + +**Interactive Mode:** + +Keeper CLI provides an interactive shell with command history, tab completion, and contextual help: + +```bash +My Vault> help # Display all available commands +My Vault> help # Get help for a specific command +My Vault> my-command --help # Display command-specific options +``` + +--- + +## Contributing + +We welcome contributions from the community! Please feel free to submit pull requests, report issues, or suggest enhancements through our [GitHub repository](https://github.com/Keeper-Security/keeper-sdk-python). + +--- + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +--- + +## Support + +For support, documentation, and additional resources: + +- **Documentation**: [Keeper Security Developer Portal](https://docs.keeper.io/) +- **Support**: [Keeper Security Support](https://www.keepersecurity.com/support.html) +- **Community**: [Keeper Security GitHub](https://github.com/Keeper-Security) \ No newline at end of file From 4e5be7c1ecbdd6e734590963bcfeaf0e4775939a Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 16 Oct 2025 17:48:08 +0530 Subject: [PATCH 36/44] Examples added --- examples/audit_report/audit_log.py | 135 +++++++++++++++++ examples/breachwatch/breachwatch_password.py | 114 ++++++++++++++ examples/folder/list_sf.py | 122 +++++++++++++++ examples/folder/transform_folder.py | 137 +++++++++++++++++ examples/miscellaneous/clipboard_copy.py | 145 ++++++++++++++++++ examples/miscellaneous/find_ownerless.py | 126 ++++++++++++++++ examples/miscellaneous/list_team.py | 130 ++++++++++++++++ examples/miscellaneous/password-report.py | 151 +++++++++++++++++++ examples/record/file_report.py | 118 +++++++++++++++ examples/record/record_history.py | 130 ++++++++++++++++ examples/record/search_record.py | 123 +++++++++++++++ examples/trash/trash_get.py | 119 +++++++++++++++ examples/trash/trash_list.py | 127 ++++++++++++++++ examples/trash/trash_purge.py | 118 +++++++++++++++ examples/trash/trash_restore.py | 126 ++++++++++++++++ examples/trash/trash_unshare.py | 126 ++++++++++++++++ 16 files changed, 2047 insertions(+) create mode 100644 examples/audit_report/audit_log.py create mode 100644 examples/breachwatch/breachwatch_password.py create mode 100644 examples/folder/list_sf.py create mode 100644 examples/folder/transform_folder.py create mode 100644 examples/miscellaneous/clipboard_copy.py create mode 100644 examples/miscellaneous/find_ownerless.py create mode 100644 examples/miscellaneous/list_team.py create mode 100644 examples/miscellaneous/password-report.py create mode 100644 examples/record/file_report.py create mode 100644 examples/record/record_history.py create mode 100644 examples/record/search_record.py create mode 100644 examples/trash/trash_get.py create mode 100644 examples/trash/trash_list.py create mode 100644 examples/trash/trash_purge.py create mode 100644 examples/trash/trash_restore.py create mode 100644 examples/trash/trash_unshare.py diff --git a/examples/audit_report/audit_log.py b/examples/audit_report/audit_log.py new file mode 100644 index 00000000..50847ec7 --- /dev/null +++ b/examples/audit_report/audit_log.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def audit_log( + context: KeeperParams, + anonymize: Optional[bool] = None, + record: Optional[str] = None, + shared_folder_uid: Optional[str] = None, + node_id: Optional[int] = None, + days: Optional[int] = None, +): + """ + Generate an audit log report. + + This function uses the Keeper CLI `AuditLogCommand` to retrieve and display + an audit log report. + """ + try: + command = AuditLogCommand() + + kwargs = { + 'target': 'json', + 'record': record, + 'shared_folder_uid': shared_folder_uid, + 'node_id': node_id, + 'days': days, + 'anonymize': anonymize, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating audit log report: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate an audit log report', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python audit_log.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + anonymize = None + record = 'record_uid' # Record name or UID + shared_folder_uid = 'shared_folder_uid' # Shared folder UID + node_id = None # Node ID + days = None # Max event age in days + + context = None + try: + context = login_to_keeper_with_config(args.config) + audit_log( + context, + anonymize=anonymize, + record=record, + shared_folder_uid=shared_folder_uid, + node_id=node_id, + days=days, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/breachwatch/breachwatch_password.py b/examples/breachwatch/breachwatch_password.py new file mode 100644 index 00000000..4119274d --- /dev/null +++ b/examples/breachwatch/breachwatch_password.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def scan_passwords( + context: KeeperParams, + passwords: Optional[List[str]] = None, +): + """ + Scan passwords against breached passwords. + + This function uses the Keeper CLI `BreachWatchPasswordCommand` to scan passwords against breached passwords + """ + try: + command = BreachWatchPasswordCommand() + + kwargs = { + 'passwords': passwords, + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error scanning passwords: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Scan passwords using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python breachwatch_password.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your password report + passwords = ["password1", "password2", "password3"] + + context = None + try: + context = login_to_keeper_with_config(args.config) + scan_passwords( + context, + passwords=passwords, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/folder/list_sf.py b/examples/folder/list_sf.py new file mode 100644 index 00000000..573ed2f4 --- /dev/null +++ b/examples/folder/list_sf.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def list_shared_folders( + context: KeeperParams, + criteria: Optional[str] = None, + verbose: Optional[bool] = None, +): + """ + List shared folders in the Keeper vault. + + This function uses the Keeper CLI `SharedFolderListCommand` to retrieve and display + shared folders. + """ + try: + command = SharedFolderListCommand() + + kwargs = { + 'pattern': criteria, + 'verbose': verbose, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error listing shared folders: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List all shared folders in the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python list_sf.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + criteria = 'folder_title_pattern' + verbose = None + + context = None + try: + context = login_to_keeper_with_config(args.config) + list_shared_folders( + context, + criteria=criteria, + verbose=verbose, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/folder/transform_folder.py b/examples/folder/transform_folder.py new file mode 100644 index 00000000..373f9def --- /dev/null +++ b/examples/folder/transform_folder.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def transform_folder( + context: KeeperParams, + folder: str, + target: str, + force: Optional[bool] = None, + link: Optional[bool] = None, + dry_run: Optional[bool] = None, + folder_type: Optional[str] = None, +): + """ + Transform a folder or move it from one location to another. + + This function uses the Keeper CLI `FolderTransformCommand` to transform a folder or move it from one location to another. + """ + try: + command = FolderTransformCommand() + + kwargs = { + 'folder': folder, + 'target': target, + 'force': force, + 'link': link, + 'dry_run': dry_run, + 'folder_type': folder_type, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error changing folder type or moving it to a new location: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Change folder type or move it to a new location', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python transform_folder.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + folder = 'folder_uid' + target = 'target_folder_uid' + force = None + link = None + dry_run = None + folder_type = 'shared' # 'shared' or 'personal' + + context = None + try: + context = login_to_keeper_with_config(args.config) + transform_folder( + context, + folder=folder, + target=target, + force=force, + link=link, + dry_run=dry_run, + folder_type=folder_type, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/miscellaneous/clipboard_copy.py b/examples/miscellaneous/clipboard_copy.py new file mode 100644 index 00000000..2b0cebdd --- /dev/null +++ b/examples/miscellaneous/clipboard_copy.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def clipboard_copy( + context: KeeperParams, + record_uid: str, + output_type: Optional[str] = None, + name: Optional[str] = None, + copy_uid: Optional[bool] = None, + login: Optional[bool] = None, + totp: Optional[bool] = None, + field: Optional[str] = None, + revision: Optional[int] = None, +): + """ + Send a record password to the clipboard. + + This function uses the Keeper CLI `ClipboardCommand` to send a record password to the clipboard. + """ + try: + command = ClipboardCommand() + + kwargs = { + 'record': record_uid, + 'output': output_type, + 'name': name, + 'copy_uid': copy_uid, + 'login': login, + 'totp': totp, + 'field': field, + 'revision': revision, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error sending record password to clipboard: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Send a record password to the clipboard', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python clipboard_copy.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + record_uid = 'record_uid' + output_type = 'clipboard' # clipboard, stdout, stdouthidden, variable + name = 'password' # Variable name if output is set to variable + copy_uid = None # Output uid instead of password + login = None # Output login name + totp = None # Output totp code + field = None # Output custom field + revision = None # Use a specific record revision + + context = None + try: + context = login_to_keeper_with_config(args.config) + clipboard_copy( + context, + record_uid=record_uid, + output_type=output_type, + name=name, + copy_uid=copy_uid, + login=login, + totp=totp, + field=field, + revision=revision, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/miscellaneous/find_ownerless.py b/examples/miscellaneous/find_ownerless.py new file mode 100644 index 00000000..05a1274b --- /dev/null +++ b/examples/miscellaneous/find_ownerless.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def find_ownerless( + context: KeeperParams, + claim: Optional[bool] = None, + folder: Optional[str] = None, + verbose: Optional[bool] = None, +): + """ + List ownerless records in the Keeper vault and optionally claim them. + + This function uses the Keeper CLI `FindOwnerlessCommand` to retrieve and display + ownerless records in the Keeper vault and optionally claim them. + """ + try: + command = FindOwnerlessCommand() + + kwargs = { + 'claim': claim, + 'folder': folder, + 'verbose': verbose, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error listing ownerless records: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List ownerless records in the Keeper vault and optionally claim them', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python find_ownerless.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + claim = None # Claim the ownerless records Boolean flag + folder = None # Folder path or UID + verbose = None # Display verbose information Boolean flag + + context = None + try: + context = login_to_keeper_with_config(args.config) + find_ownerless( + context, + claim=claim, + folder=folder, + verbose=verbose, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/miscellaneous/list_team.py b/examples/miscellaneous/list_team.py new file mode 100644 index 00000000..6490b8e5 --- /dev/null +++ b/examples/miscellaneous/list_team.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def list_teams( + context: KeeperParams, + verbose: Optional[bool] = None, + very_verbose: Optional[bool] = None, + all_teams: Optional[bool] = None, + sort: Optional[str] = None, +): + """ + List teams in the Keeper vault. + + This function uses the Keeper CLI `TeamListCommand` to retrieve and display + teams. + """ + try: + command = TeamListCommand() + + kwargs = { + 'verbose': verbose, + 'very_verbose': very_verbose, + 'all': all_teams, + 'sort': sort, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error listing teams: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List all teams in the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python list_team.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + verbose = None + very_verbose = None + all_teams = None + sort = 'company' # company, team_uid, name + + context = None + try: + context = login_to_keeper_with_config(args.config) + list_teams( + context, + verbose=verbose, + all_teams=all_teams, + very_verbose=very_verbose, + sort=sort, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/miscellaneous/password-report.py b/examples/miscellaneous/password-report.py new file mode 100644 index 00000000..96f165ba --- /dev/null +++ b/examples/miscellaneous/password-report.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def password_report( + context: KeeperParams, + format: Optional[str] = None, + output_path: Optional[str] = None, + policy: Optional[str] = None, + length: Optional[int] = None, + upper: Optional[int] = None, + lower: Optional[int] = None, + digits: Optional[int] = None, + special: Optional[int] = None, + folder: Optional[str] = None, + verbose: Optional[bool] = None, +): + """ + Generate a report of passwords in the Keeper vault. + + This function uses the Keeper CLI `PasswordReportCommand` to retrieve and display + records based on the provided criteria and filters. + """ + try: + command = PasswordReportCommand() + + kwargs = { + 'format': format, + 'output': output_path, + 'policy': policy, + 'length': length, + 'upper': upper, + 'lower': lower, + 'digits': digits, + 'special': special, + 'folder': folder, + 'verbose': verbose, + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error generating password report: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate a report of passwords in the Keeper vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python password_report.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Example parameters - customize these for your password report + format = "table" # Format of output (table, json, csv) + output_path = "password_report.csv" # Path to output file for csv format + policy = "12,2,2,2,0" # Password complexity policy. Length,Lower,Upper,Digits,Special. Default is 12,2,2,2,0 + length = 12 # Minimum password length. + upper = 2 # Minimum uppercase characters. + lower = 2 # Minimum lowercase characters. + digits = 2 # Minimum digits. + special = 0 # Minimum special characters. + folder = 'folder_uid' # Folder path or UID + verbose = None # Display verbose information + + context = None + try: + context = login_to_keeper_with_config(args.config) + password_report( + context, + format=format, + output_path=output_path, + policy=policy, + length=length, + upper=upper, + lower=lower, + digits=digits, + special=special, + folder=folder, + verbose=verbose, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/file_report.py b/examples/record/file_report.py new file mode 100644 index 00000000..2414df41 --- /dev/null +++ b/examples/record/file_report.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def file_report( + context: KeeperParams, + try_download: Optional[bool] = None, +): + """ + List file attachments for all records in the Keeper vault. + + This function uses the Keeper CLI `RecordFileReportCommand` to retrieve and display + file attachments for all records in the Keeper vault. + """ + try: + command = RecordFileReportCommand() + + kwargs = { + 'try_download': try_download, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating file report: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate a file report for records with file attachments', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python file_report.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + try_download = None + + context = None + try: + context = login_to_keeper_with_config(args.config) + file_report( + context, + try_download=try_download, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/record_history.py b/examples/record/record_history.py new file mode 100644 index 00000000..60daf6f3 --- /dev/null +++ b/examples/record/record_history.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def record_history( + context: KeeperParams, + record_uid: str, + action: Optional[str] = None, + revision: Optional[int] = None, + verbose: Optional[bool] = None, +): + """ + List the history of a record revisions. + + This function uses the Keeper CLI `RecordHistoryCommand` to retrieve and display + the history of a record revisions. + """ + try: + command = RecordHistoryCommand() + + kwargs = { + 'record': record_uid, + 'action': action, + 'revision': revision, + 'verbose': verbose, + } + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error loading record history: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List the history of a record revisions', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python record_history.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + record_uid = 'record_uid' + action = 'list' # list, view, diff, restore + revision = None # int value of the revision to view + verbose = None + + context = None + try: + context = login_to_keeper_with_config(args.config) + record_history( + context, + record_uid=record_uid, + action=action, + revision=revision, + verbose=verbose, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/record/search_record.py b/examples/record/search_record.py new file mode 100644 index 00000000..02b54df4 --- /dev/null +++ b/examples/record/search_record.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def search( + vault: vault_online.VaultOnline, + pattern: str, + record_type: Optional[str] = None, + record_version: Optional[int] = None, +): + """ + Search for records which match the search pattern. + + This function retrieves a list of records which match the search pattern. + using the Keeper SDK function. + """ + try: + records = vault.vault_data.find_records(criteria=pattern, record_type=record_type, record_version=record_version) + print(f'Found {len(records)} records') + for record in records: + print(f'{record.title} ({record.record_uid}) {record.record_type} {record.record_version}') + return True + except Exception as e: + print(f'Error searching for records: {str(e)}') + return False + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Search for records using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python search_record.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + pattern = "record_pattern" # Title, uid, path, or pattern + record_type = "record_type" # record_type + record_version = "record_version" # record_version int value + + print(f"Note: This example will attempt to search for records matching '{pattern}'") + + context = None + try: + context = login_to_keeper_with_config(args.config) + success = search( + vault=context.vault, + pattern=pattern, + record_type=record_type, + record_version=record_version, + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/trash/trash_get.py b/examples/trash/trash_get.py new file mode 100644 index 00000000..62b1880d --- /dev/null +++ b/examples/trash/trash_get.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def get_trash_record( + context: KeeperParams, + record_uid: str, +): + """ + Get and display a record from the Keeper vault trash. + + This function uses the Keeper CLI `TrashGetCommand` to retrieve and display + records based on the provided criteria and filters. + """ + try: + get_command = TrashGetCommand() + + kwargs = { + 'record': record_uid, + } + + get_command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error getting trashed record: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Get a record from the Keeper vault trash using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python trash_get.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_uid = "record_uid" # Replace with actual trashed record UID + + print(f"Note: This example will attempt to get details for trashed record '{record_uid}'") + + context = None + try: + context = login_to_keeper_with_config(args.config) + get_trash_record( + context, + record_uid=record_uid, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/trash/trash_list.py b/examples/trash/trash_list.py new file mode 100644 index 00000000..81322c08 --- /dev/null +++ b/examples/trash/trash_list.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def list_trash_records( + context: KeeperParams, + show_details: Optional[bool] = None, + criteria: Optional[str] = None, + format: str = 'table', + output_path: Optional[str] = None +): + """ + List and display records from the Keeper trash with optional filtering. + + This function uses the Keeper CLI `TrashListCommand` to retrieve and display + records based on the provided criteria and filters. + """ + try: + list_command = TrashListCommand() + + kwargs = { + 'verbose': show_details, + 'format': format, + 'pattern': criteria, + 'output': output_path, + } + + list_command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error listing trashed records: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='List all trashed records and folders in the trash using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python trash_list.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + show_details = None # Display verbose information Boolean flag + criteria = None # Search pattern + format = 'table' # Format of output (table, json, csv) + output_path = None # Path to output file for csv format + + context = None + try: + context = login_to_keeper_with_config(args.config) + list_trash_records( + context, + show_details=show_details, + criteria=criteria, + format=format, + output_path=output_path, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/trash/trash_purge.py b/examples/trash/trash_purge.py new file mode 100644 index 00000000..f97d5bd8 --- /dev/null +++ b/examples/trash/trash_purge.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def purge_trash( + context: KeeperParams, + force: bool, +): + """ + Purge all records from the Keeper vault trash. + + This function uses the Keeper CLI `TrashPurgeCommand` to purge all records from the trash + records based on the provided criteria and filters. + """ + try: + purge_command = TrashPurgeCommand() + + kwargs = { + 'force': force, + } + + purge_command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error purging trash: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Purge all records from the Keeper vault trash using Keeper CLI package', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python trash_purge.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Bool flags can be set to True or None (to be sent as False) + force = None + + context = None + try: + context = login_to_keeper_with_config(args.config) + purge_trash( + context, + force=force, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/trash/trash_restore.py b/examples/trash/trash_restore.py new file mode 100644 index 00000000..1355c06a --- /dev/null +++ b/examples/trash/trash_restore.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def restore_trash_record( + context: KeeperParams, + record_uid: str, + force: Optional[bool] = None, +): + """ + Restore a record from the Keeper vault trash. + + This function uses the Keeper CLI `TrashRestoreCommand` to restore a trashed record + records based on the provided criteria and filters. + """ + try: + restore_command = TrashRestoreCommand() + + kwargs = { + 'records': [record_uid], + 'force': force, + } + + restore_command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error restoring trashed record: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Restore a record from the Keeper vault trash using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python trash_restore.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_uid = "record_uid" # Replace with actual trashed record UID + + # Bool flags can be set to True or None (to be sent as False) + force = None + + print(f"Note: This example will attempt to restore the trashed record '{record_uid}'") + + context = None + try: + context = login_to_keeper_with_config(args.config) + restore_trash_record( + context, + record_uid=record_uid, + force=force, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file diff --git a/examples/trash/trash_unshare.py b/examples/trash/trash_unshare.py new file mode 100644 index 00000000..eb6a6056 --- /dev/null +++ b/examples/trash/trash_unshare.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def unshare_trash_record( + context: KeeperParams, + record_uid: str, + force: Optional[bool] = None, +): + """ + Unshare a shared trashed record from the Keeper vault trash. + + This function uses the Keeper CLI `TrashUnshareCommand` to unshare a shared trashed record + records based on the provided criteria and filters. + """ + try: + unshare_command = TrashUnshareCommand() + + kwargs = { + 'records': [record_uid], + 'force': force, + } + + unshare_command.execute(context=context, **kwargs) + return True + + except Exception as e: + print(f'Error unsharing record: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Unshare a shared trashed record from the Keeper vault trash using Keeper CLI package', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python trash_unshare.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + record_uid = 'record_uid' # Record UID to unshare + + # Bool flags can be set to True or None (to be sent as False) + force = None + + print(f"Note: This example will attempt to unshare a shared trashed record '{record_uid}'") + + context = None + try: + context = login_to_keeper_with_config(args.config) + unshare_trash_record( + context, + record_uid=record_uid, + force=force, + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() \ No newline at end of file From c2b54b5d96c92eaa7371a629b7e5176200769500 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 20 Oct 2025 10:17:21 +0530 Subject: [PATCH 37/44] Create user command added --- .../commands/enterprise_create_user.py | 269 ++++++++++ .../src/keepercli/register_commands.py | 3 +- .../enterprise/enterprise_user_management.py | 484 ++++++++++++++++++ 3 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 keepercli-package/src/keepercli/commands/enterprise_create_user.py create mode 100644 keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py diff --git a/keepercli-package/src/keepercli/commands/enterprise_create_user.py b/keepercli-package/src/keepercli/commands/enterprise_create_user.py new file mode 100644 index 00000000..50e4a3bb --- /dev/null +++ b/keepercli-package/src/keepercli/commands/enterprise_create_user.py @@ -0,0 +1,269 @@ +import argparse +from typing import Optional +from urllib.parse import urlunparse + +from . import base +from .. import api +from ..params import KeeperParams +from keepersdk.vault import vault_record +from keepersdk.enterprise.enterprise_user_management import EnterpriseUserManager, CreateUserResponse +from .share_management import OneTimeShareCreateCommand + +# Constants +DEFAULT_ONE_TIME_SHARE_EXPIRY = '7d' +ONE_TIME_SHARE_LABEL = 'One-Time Share' +ONE_TIME_SHARE_FIELD_TYPE = 'url' +VAULT_URL_PATH = '/vault' +PASSWORD_CHANGE_NOTE = ( + 'The user is required to change their Master Password ' + 'upon login.' +) + +# Logging format constants +LOG_FORMAT_VERBOSE_HEADER = 'The account {} has been created. Login details below:' +LOG_FORMAT_VERBOSE_SUCCESS = 'User "{}" has been created with ID {}' +LOG_FORMAT_FIELD_WIDTH = 24 + +logger = api.get_logger() + +class CreateEnterpriseUserCommand(base.ArgparseCommand): + """Create an enterprise user command.""" + + def __init__(self): + parser = argparse.ArgumentParser( + prog='create-user', + description='Create an enterprise user.' + ) + CreateEnterpriseUserCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + """Add command line arguments to parser.""" + parser.add_argument('email', help='User email') + parser.add_argument( + '--name', dest='full_name', action='store', + help='user name' + ) + parser.add_argument( + '--node', dest='node', action='store', + help='node name or node ID' + ) + parser.add_argument( + '--folder', dest='folder', action='store', + help='folder name or UID to store password record' + ) + parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', + help='print verbose information' + ) + + def _create_enterprise_user_manager(self, context: KeeperParams) -> EnterpriseUserManager: + """ + Create an EnterpriseUserManager instance from KeeperParams context. + """ + return EnterpriseUserManager( + loader=context.enterprise_loader, + auth_context=context.auth + ) + + def _add_one_time_share( + self, + context: KeeperParams, + record_uid: str, + email: str + ) -> Optional[str]: + """ + Create and add one-time share link to the record. + + Args: + context: Keeper parameters context + record_uid: UID of the record to share + email: Email address for the share name + + Returns: + One-time share URL if successful, None otherwise + """ + try: + if not self._validate_record_exists(context, record_uid): + return None + + ots_url = self._create_one_time_share_url(context, record_uid, email) + if ots_url: + self._add_share_url_to_record(context, record_uid, ots_url) + + return ots_url + except Exception as e: + logger.warning(f"Could not create one-time share: {e}") + return None + + def _validate_record_exists(self, context: KeeperParams, record_uid: str) -> bool: + """Validate that the record exists in the vault.""" + record_data = context.vault.vault_data.get_record(record_uid) + if not record_data: + logger.warning(f"Could not load record {record_uid} for one-time share") + return False + return True + + def _create_one_time_share_url( + self, + context: KeeperParams, + record_uid: str, + email: str + ) -> Optional[str]: + """Create one-time share URL for the record.""" + ots_command = OneTimeShareCreateCommand() + return ots_command.execute( + context, + record=record_uid, + share_name=f'{email}: Master Password', + expire=DEFAULT_ONE_TIME_SHARE_EXPIRY + ) + + def _add_share_url_to_record( + self, + context: KeeperParams, + record_uid: str, + ots_url: str + ) -> None: + """Add one-time share URL as a custom field to the record.""" + from keepersdk.vault import record_management + + full_record = context.vault.vault_data.load_record(record_uid) + + if isinstance(full_record, vault_record.TypedRecord): + ots_field = vault_record.TypedField() + ots_field.type = ONE_TIME_SHARE_FIELD_TYPE + ots_field.label = ONE_TIME_SHARE_LABEL + ots_field.value = [ots_url] + full_record.custom.append(ots_field) + record_management.update_record(context.vault, full_record) + context.vault.sync_down() + + def _log_results( + self, + result: CreateUserResponse, + displayname: str, + keeper_url: str, + notes: str, + verbose: bool + ) -> None: + """ + Log the results of user creation. + + Args: + result: User creation response + displayname: User display name + keeper_url: Keeper vault URL + notes: Additional notes to display + verbose: Whether to show verbose output + """ + if verbose: + self._log_verbose_results(result, displayname, keeper_url, notes) + else: + self._log_simple_results(result) + + def _log_verbose_results( + self, + result: CreateUserResponse, + displayname: str, + keeper_url: str, + notes: str + ) -> None: + """Log verbose user creation results.""" + logger.info(LOG_FORMAT_VERBOSE_HEADER.format(result.email)) + + field_width = LOG_FORMAT_FIELD_WIDTH + logger.info(f'{"Vault Login URL:":>{field_width}s} {keeper_url}') + logger.info(f'{"Email:":>{field_width}s} {result.email}') + + if displayname: + logger.info(f'{"Name:":>{field_width}s} {displayname}') + if result.node_id: + logger.info(f'{"Node ID:":>{field_width}s} {result.node_id}') + + logger.info(f'{"Master Password:":>{field_width}s} {result.generated_password}') + logger.info(f'{"Note:":>{field_width}s} {notes}') + + def _log_simple_results(self, result: CreateUserResponse) -> None: + """Log simple user creation results.""" + logger.info( + LOG_FORMAT_VERBOSE_SUCCESS.format( + result.email, + result.enterprise_user_id + ) + ) + + def execute(self, context: KeeperParams, **kwargs): + """ + Execute the create user command. + + Args: + context: Keeper parameters context + **kwargs: Command line arguments + + Returns: + Enterprise user ID if successful, None otherwise + + Raises: + CommandError: If user creation fails + """ + self._validate_context(context) + + email = kwargs.get('email') + displayname = kwargs.get('full_name', '') + node_name = kwargs.get('node') + verbose = kwargs.get('verbose', False) + + try: + result = self._create_user(context, email, displayname, node_name) + keeper_url = self._build_keeper_url(context.server, email) + + self._log_results( + result, displayname, keeper_url, PASSWORD_CHANGE_NOTE, verbose + ) + + return result.enterprise_user_id + + except ValueError as e: + logger.error(str(e)) + return None + except Exception as e: + if "already exists" in str(e): + raise base.CommandError(str(e)) + else: + raise base.CommandError(f"Failed to create user: {str(e)}") + + def _validate_context(self, context: KeeperParams) -> None: + """Validate that required context data is available.""" + assert context.enterprise_data is not None + assert context.auth is not None + + def _create_user( + self, + context: KeeperParams, + email: str, + displayname: str, + node_name: Optional[str] + ) -> CreateUserResponse: + """Create the enterprise user.""" + user_manager = self._create_enterprise_user_manager(context) + + from keepersdk.enterprise.enterprise_user_management import CreateUserRequest + request = CreateUserRequest( + email=email, + display_name=displayname, + node_name=node_name + ) + return user_manager.create_user(request) + + def _build_keeper_url(self, server: str, email: str) -> str: + """Build the Keeper vault URL for the user.""" + return urlunparse(( + 'https', + server, + VAULT_URL_PATH, + None, + None, + f'email/{email}' + )) diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 1583d3aa..ef47ae95 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -77,9 +77,10 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Enterprise): - from .commands import (enterprise_info, enterprise_node, enterprise_role, enterprise_team, enterprise_user, + from .commands import (enterprise_info, enterprise_node, enterprise_role, enterprise_team, enterprise_user, enterprise_create_user, importer_commands, audit_report, audit_alert, audit_log) + commands.register_command('create-user', enterprise_create_user.CreateEnterpriseUserCommand(), base.CommandScope.Enterprise, 'ecu') commands.register_command('enterprise-down', enterprise_info.EnterpriseDownCommand(), base.CommandScope.Enterprise, 'ed') commands.register_command('enterprise-info', enterprise_info.EnterpriseInfoCommand(), base.CommandScope.Enterprise, 'ei') commands.register_command('enterprise-node', enterprise_node.EnterpriseNodeCommand(), base.CommandScope.Enterprise, 'en') diff --git a/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py b/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py new file mode 100644 index 00000000..0d507e9e --- /dev/null +++ b/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py @@ -0,0 +1,484 @@ + +"""Enterprise user management functionality for Keeper SDK.""" + +import json +import re +from typing import Optional +from dataclasses import dataclass + +from keepersdk.authentication import keeper_auth + +from . import enterprise_types +from .. import utils, crypto, generator +from ..proto import enterprise_pb2 + +# Constants +EMAIL_PATTERN = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' +PBKDF2_ITERATIONS = 1_000_000 +DEFAULT_PASSWORD_LENGTH = 20 +SALT_LENGTH = 16 +AUTH_VERIFIER_SALT_LENGTH = 16 + +# Error codes +ERROR_CODE_EXISTS = "exists" +ERROR_CODE_SUCCESS = "success" +ERROR_CODE_OK = "ok" + +# Documentation URLs +DOMAIN_RESERVATION_DOC_URL = ( + 'https://docs.keeper.io/enterprise-guide/' + 'user-and-team-provisioning/email-auto-provisioning' +) + +# Error messages +ERROR_MSG_INVALID_EMAIL = "Invalid email format: {}" +ERROR_MSG_NODE_RESOLUTION_FAILED = "Node resolution failed: {}" +ERROR_MSG_CANNOT_DETERMINE_ROOT_NODE = "Cannot determine root node" +ERROR_MSG_NODE_NOT_FOUND_BY_ID = "Node with ID {} not found" +ERROR_MSG_NODE_NOT_FOUND_BY_NAME = "Node '{}' not found" +ERROR_MSG_MULTIPLE_NODES_FOUND = "Multiple nodes found with name '{}'" +ERROR_MSG_PROVISION_REQUEST_FAILED = "Failed to create provision request: {}" +ERROR_MSG_API_CALL_FAILED = "API call failed: {}" +ERROR_MSG_USER_EXISTS = 'User "{}" already exists' +ERROR_MSG_AUTO_CREATE_FAILED = ( + 'Failed to auto-create account "{}".\n' + 'Creating user accounts without email verification is ' + 'only permitted on reserved domains.\n' + 'To reserve a domain please contact Keeper support. ' + 'Learn more about domain reservation here:\n{}' +) + + +@dataclass +class CreateUserRequest: + """Request parameters for creating an enterprise user.""" + email: str + display_name: Optional[str] = None + node_id: Optional[int] = None + node_name: Optional[str] = None + password_length: int = DEFAULT_PASSWORD_LENGTH + suppress_email_invite: bool = False + + +@dataclass +class CreateUserResponse: + """Response from enterprise user creation.""" + enterprise_user_id: int + email: str + generated_password: str + display_name: Optional[str] = None + node_id: int = 0 + success: bool = True + message: Optional[str] = None + verification_code: Optional[str] = None + + +class EnterpriseUserCreationError(Exception): + """Exception raised when enterprise user creation fails.""" + + def __init__(self, message: str, code: Optional[str] = None): + self.message = message + self.code = code + super().__init__(self.message) + + +class EnterpriseUserManager: + """Manages enterprise user creation operations.""" + + def __init__(self, loader: enterprise_types.IEnterpriseLoader, auth_context: keeper_auth.KeeperAuth): + """Initialize the enterprise user manager. + + Args: + loader: Enterprise data loader interface + auth_context: Authentication context for API calls + """ + self.loader = loader + self.auth = auth_context + + def validate_email(self, email: str) -> bool: + """Validate email format. + + Args: + email: Email address to validate + + Returns: + True if email is valid, False otherwise + """ + if not email: + return False + + return bool(re.match(EMAIL_PATTERN, email)) + + def resolve_node_id(self, node_name_or_id: Optional[str] = None) -> int: + """Resolve node ID from name or ID string. + + Args: + node_name_or_id: Node name or ID, None for root node + + Returns: + Resolved node ID + + Raises: + EnterpriseUserCreationError: If node cannot be resolved + """ + if not node_name_or_id: + return self._get_root_node_id() + + if self._is_numeric_id(node_name_or_id): + return self._resolve_node_by_id(int(node_name_or_id)) + + return self._resolve_node_by_name(node_name_or_id) + + def _get_root_node_id(self) -> int: + """Get the root node ID.""" + root_node = self.loader.enterprise_data.root_node + if root_node: + return root_node.node_id + raise EnterpriseUserCreationError(ERROR_MSG_CANNOT_DETERMINE_ROOT_NODE) + + def _is_numeric_id(self, node_identifier: str) -> bool: + """Check if the node identifier is numeric.""" + try: + int(node_identifier) + return True + except ValueError: + return False + + def _resolve_node_by_id(self, node_id: int) -> int: + """Resolve node by numeric ID.""" + enterprise_data = self.loader.enterprise_data + if enterprise_data.nodes.get_entity(node_id): + return node_id + raise EnterpriseUserCreationError(ERROR_MSG_NODE_NOT_FOUND_BY_ID.format(node_id)) + + def _resolve_node_by_name(self, node_name: str) -> int: + """Resolve node by name.""" + enterprise_data = self.loader.enterprise_data + matching_nodes = [ + node for node in enterprise_data.nodes.get_all_entities() + if node.name == node_name + ] + + if len(matching_nodes) == 0: + raise EnterpriseUserCreationError(ERROR_MSG_NODE_NOT_FOUND_BY_NAME.format(node_name)) + elif len(matching_nodes) > 1: + raise EnterpriseUserCreationError(ERROR_MSG_MULTIPLE_NODES_FOUND.format(node_name)) + + return matching_nodes[0].node_id + + def create_provision_request( + self, + request: CreateUserRequest, + resolved_node_id: int + ) -> tuple[enterprise_pb2.EnterpriseUsersProvisionRequest, str]: + """Create a user provision request with cryptographic setup. + + Args: + request: User creation request parameters + resolved_node_id: Resolved node ID for user placement + + Returns: + Tuple of (provision_request, generated_password) + + Raises: + EnterpriseUserCreationError: If request creation fails + """ + try: + enterprise_data = self.loader.enterprise_data + tree_key = enterprise_data.enterprise_info.tree_key + + rq = enterprise_pb2.EnterpriseUsersProvisionRequest() + rq.clientVersion = self.auth.keeper_endpoint.client_version + + # Generate user data and password + user_data, user_password, user_data_key = self._generate_user_credentials(request) + enterprise_user_id = self.loader.get_enterprise_id() + + # Create user provision request + user_rq = self._create_user_provision_request( + request, resolved_node_id, enterprise_user_id, + user_data, user_data_key, tree_key, user_password + ) + + rq.users.append(user_rq) + return rq, user_password + + except Exception as e: + raise EnterpriseUserCreationError(ERROR_MSG_PROVISION_REQUEST_FAILED.format(str(e))) + + def _generate_user_credentials(self, request: CreateUserRequest) -> tuple[bytes, str, bytes]: + """Generate user data, password, and data key.""" + data = {'displayname': request.display_name or request.email} + user_data = json.dumps(data).encode('utf-8') + user_password = generator.KeeperPasswordGenerator( + length=request.password_length + ).generate() + user_data_key = utils.generate_aes_key() + return user_data, user_password, user_data_key + + def _create_user_provision_request( + self, + request: CreateUserRequest, + resolved_node_id: int, + enterprise_user_id: int, + user_data: bytes, + user_data_key: bytes, + tree_key: bytes, + user_password: str + ) -> enterprise_pb2.EnterpriseUsersProvision: + """Create the user provision request object.""" + user_rq = enterprise_pb2.EnterpriseUsersProvision() + user_rq.enterpriseUserId = enterprise_user_id + user_rq.username = request.email + user_rq.nodeId = resolved_node_id + user_rq.encryptedData = utils.base64_url_encode( + crypto.encrypt_aes_v1(user_data, tree_key) + ) + user_rq.keyType = enterprise_pb2.KT_ENCRYPTED_BY_DATA_KEY + + # Set up enterprise data key + enterprise_ec_key = self._get_enterprise_ec_key() + user_rq.enterpriseUsersDataKey = crypto.encrypt_ec( + user_data_key, enterprise_ec_key + ) + + # Set up authentication and encryption + self._setup_user_authentication(user_rq, user_password, user_data_key) + + # Set up cryptographic keys + self._setup_cryptographic_keys(user_rq, user_data_key) + + # Set up device token and client key + user_rq.encryptedDeviceToken = self.auth.auth_context.device_token + user_rq.encryptedClientKey = crypto.encrypt_aes_v1( + utils.generate_aes_key(), user_data_key + ) + + return user_rq + + def _get_enterprise_ec_key(self): + """Get the enterprise EC public key.""" + enterprise_data = self.loader.enterprise_data + enterprise_ec_key = enterprise_data.enterprise_info.ec_public_key + if not enterprise_ec_key: + enterprise_ec_key = crypto.load_ec_public_key( + utils.base64_url_decode( + self.auth.auth_context.enterprise_ec_public_key + ) + ) + return enterprise_ec_key + + def _setup_user_authentication( + self, + user_rq: enterprise_pb2.EnterpriseUsersProvision, + user_password: str, + user_data_key: bytes + ) -> None: + """Set up user authentication verifier and encryption parameters.""" + user_rq.authVerifier = utils.create_auth_verifier( + user_password, + crypto.get_random_bytes(AUTH_VERIFIER_SALT_LENGTH), + PBKDF2_ITERATIONS + ) + user_rq.encryptionParams = utils.create_encryption_params( + user_password, + crypto.get_random_bytes(SALT_LENGTH), + PBKDF2_ITERATIONS, + user_data_key + ) + + def _setup_cryptographic_keys( + self, + user_rq: enterprise_pb2.EnterpriseUsersProvision, + user_data_key: bytes + ) -> None: + """Set up RSA and EC cryptographic keys for the user.""" + # Set up RSA keys if not forbidden + if not self.auth.auth_context.forbid_rsa: + self._setup_rsa_keys(user_rq, user_data_key) + + # Set up EC keys + self._setup_ec_keys(user_rq, user_data_key) + + def _setup_rsa_keys( + self, + user_rq: enterprise_pb2.EnterpriseUsersProvision, + user_data_key: bytes + ) -> None: + """Set up RSA keys for the user.""" + rsa_private_key, rsa_public_key = crypto.generate_rsa_key() + rsa_private = crypto.unload_rsa_private_key(rsa_private_key) + rsa_public = crypto.unload_rsa_public_key(rsa_public_key) + user_rq.rsaPublicKey = rsa_public + user_rq.rsaEncryptedPrivateKey = crypto.encrypt_aes_v1( + rsa_private, user_data_key + ) + + def _setup_ec_keys( + self, + user_rq: enterprise_pb2.EnterpriseUsersProvision, + user_data_key: bytes + ) -> None: + """Set up EC keys for the user.""" + ec_private_key, ec_public_key = crypto.generate_ec_key() + ec_private = crypto.unload_ec_private_key(ec_private_key) + ec_public = crypto.unload_ec_public_key(ec_public_key) + user_rq.eccPublicKey = ec_public + user_rq.eccEncryptedPrivateKey = crypto.encrypt_aes_v2( + ec_private, user_data_key + ) + + def execute_provision_request( + self, + provision_request: enterprise_pb2.EnterpriseUsersProvisionRequest, + email: str + ) -> enterprise_pb2.EnterpriseUsersProvisionResponse: + """Execute the user provision request via API. + + Args: + provision_request: The provision request to execute + email: User email for error reporting + + Returns: + Provision response from server + + Raises: + EnterpriseUserCreationError: If provisioning fails + """ + try: + rs = self.auth.execute_auth_rest( + 'enterprise/enterprise_user_provision', + provision_request, + response_type=enterprise_pb2.EnterpriseUsersProvisionResponse + ) + + self._validate_provision_response(rs, email) + return rs + + except Exception as e: + if isinstance(e, EnterpriseUserCreationError): + raise + raise EnterpriseUserCreationError(ERROR_MSG_API_CALL_FAILED.format(str(e))) + + def _validate_provision_response( + self, + response: enterprise_pb2.EnterpriseUsersProvisionResponse, + email: str + ) -> None: + """Validate the provision response and raise appropriate errors.""" + for user_rs in response.results: + if user_rs.code == ERROR_CODE_EXISTS: + raise EnterpriseUserCreationError( + ERROR_MSG_USER_EXISTS.format(email), + code=ERROR_CODE_EXISTS + ) + if user_rs.code and user_rs.code not in [ERROR_CODE_SUCCESS, ERROR_CODE_OK]: + raise EnterpriseUserCreationError( + ERROR_MSG_AUTO_CREATE_FAILED.format(email, DOMAIN_RESERVATION_DOC_URL), + code=user_rs.code + ) + + def create_user(self, request: CreateUserRequest) -> CreateUserResponse: + """Create a new enterprise user. + + Args: + request: User creation request parameters + + Returns: + CreateUserResponse with user details and generated password + + Raises: + EnterpriseUserCreationError: If user creation fails + """ + self._validate_user_request(request) + + resolved_node_id = self._resolve_user_node(request) + + provision_request, user_password = self.create_provision_request( + request, resolved_node_id + ) + + response = self.execute_provision_request(provision_request, request.email) + + # Reload enterprise data to get updated user info + self.loader.load() + + return self._build_user_response(request, response, user_password, resolved_node_id) + + def _validate_user_request(self, request: CreateUserRequest) -> None: + """Validate the user creation request.""" + if not self.validate_email(request.email): + raise EnterpriseUserCreationError(ERROR_MSG_INVALID_EMAIL.format(request.email)) + + def _resolve_user_node(self, request: CreateUserRequest) -> int: + """Resolve the node for the user.""" + try: + # Use node_id if provided, otherwise use node_name + node_identifier = None + if request.node_id: + node_identifier = str(request.node_id) + elif request.node_name: + node_identifier = request.node_name + + return self.resolve_node_id(node_identifier) + except Exception as e: + raise EnterpriseUserCreationError(ERROR_MSG_NODE_RESOLUTION_FAILED.format(str(e))) + + def _build_user_response( + self, + request: CreateUserRequest, + response: enterprise_pb2.EnterpriseUsersProvisionResponse, + user_password: str, + resolved_node_id: int + ) -> CreateUserResponse: + """Build the user creation response.""" + result = response.results[0] if response.results else None + + return CreateUserResponse( + enterprise_user_id=result.enterpriseUserId if result else 0, + email=request.email, + generated_password=user_password, + display_name=request.display_name, + node_id=resolved_node_id, + success=True, + message=result.message if result else None, + verification_code=getattr(result, 'verificationCode', None) if result else None + ) + + +def create_enterprise_user( + loader: enterprise_types.IEnterpriseLoader, + auth_context: keeper_auth.KeeperAuth, + email: str, + display_name: Optional[str] = None, + node_id: Optional[int] = None, + password_length: int = DEFAULT_PASSWORD_LENGTH, + suppress_email_invite: bool = False +) -> CreateUserResponse: + """Convenience function to create an enterprise user. + + Args: + loader: Enterprise data loader + auth_context: Authentication context + email: User email address + display_name: Optional display name + node_id: Optional node ID (uses root node if None) + password_length: Length of generated password (default 20) + suppress_email_invite: Whether to suppress email invitation + + Returns: + CreateUserResponse with user details + + Raises: + EnterpriseUserCreationError: If user creation fails + """ + request = CreateUserRequest( + email=email, + display_name=display_name, + node_id=node_id, + password_length=password_length, + suppress_email_invite=suppress_email_invite + ) + + manager = EnterpriseUserManager(loader, auth_context) + return manager.create_user(request) \ No newline at end of file From ab037d8d8273fe99913d49affa8eaa5b78d84d29 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 27 Oct 2025 17:58:48 +0530 Subject: [PATCH 38/44] Find duplicate command added --- keepercli-package/setup.cfg | 4 + .../keepercli/commands/enterprise_utils.py | 14 + .../commands/record_handling_commands.py | 412 +++++++++- .../keepercli/commands/share_management.py | 33 +- .../src/keepercli/helpers/share_record.py | 85 ++ .../src/keepercli/helpers/share_utils.py | 771 +++++++++++------- .../src/keepercli/register_commands.py | 1 + .../src/keepersdk/vault/record_management.py | 5 +- 8 files changed, 1007 insertions(+), 318 deletions(-) create mode 100644 keepercli-package/src/keepercli/helpers/share_record.py diff --git a/keepercli-package/setup.cfg b/keepercli-package/setup.cfg index 3d8c41bf..877a1315 100644 --- a/keepercli-package/setup.cfg +++ b/keepercli-package/setup.cfg @@ -44,3 +44,7 @@ keepercli = [options.extras_require] import = pykeepass + +[options.entry_points] +console_scripts = + keeper = keepercli.__main__:main diff --git a/keepercli-package/src/keepercli/commands/enterprise_utils.py b/keepercli-package/src/keepercli/commands/enterprise_utils.py index d2ea3f6e..22505355 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_utils.py +++ b/keepercli-package/src/keepercli/commands/enterprise_utils.py @@ -6,7 +6,9 @@ from keepersdk.enterprise import enterprise_types, enterprise_constants from keepersdk.proto import enterprise_pb2 from . import base +from ..params import KeeperParams +BUSINESS_TRIAL = 'business_trial' class NodeUtils: @staticmethod @@ -498,6 +500,18 @@ def get_managed_nodes_for_user(enterprise_data: enterprise_types.IEnterpriseData result[x.managed_node_id] = x.cascade_node_management return result + +def is_addon_enabled(context: KeeperParams, addon_name: str) -> bool: + keeper_licenses = context.enterprise_data.licenses.get_all_entities() + for license in keeper_licenses: + license.license_status + if not keeper_licenses: + raise base.CommandError('No licenses found') + if next(iter(keeper_licenses), {}).license_status == BUSINESS_TRIAL: + return True + addons = [a for l in keeper_licenses for a in l.add_ons if a.name == addon_name] + return any(a for a in addons if a.enabled or a.included_in_product) + ''' def _load_managed_nodes(self, context: KeeperParams) -> None: enterprise_data = context.enterprise_data diff --git a/keepercli-package/src/keepercli/commands/record_handling_commands.py b/keepercli-package/src/keepercli/commands/record_handling_commands.py index ef217ac3..966aad69 100644 --- a/keepercli-package/src/keepercli/commands/record_handling_commands.py +++ b/keepercli-package/src/keepercli/commands/record_handling_commands.py @@ -1,18 +1,20 @@ import argparse import datetime +import hashlib import json import re from typing import Optional, List +import urllib from colorama import Fore, Back, Style from keepersdk.proto import record_pb2 -from keepersdk.vault import (record_types, vault_record, vault_online) +from keepersdk.vault import (record_types, vault_record, vault_online, record_management) from keepersdk import crypto, utils from . import base -from ..helpers import folder_utils, record_utils, report_utils -from .. import api +from ..helpers import folder_utils, record_utils, report_utils, share_utils +from .. import api, prompt_utils from ..params import KeeperParams @@ -20,6 +22,36 @@ MAX_VERSION_COUNT = 5 TRUNCATE_LENGTH = 52 +# Constants for FindDuplicateCommand +TEAM_USER_TYPE = '(Team User)' +NON_SHARED_LABEL = 'non-shared' +ENTERPRISE_COMPLIANCE_DAYS = 1 +URL_DISPLAY_LENGTH = 30 +ENTERPRISE_UPDATE_FLOOR_DAYS = 1 + +# Default field mappings for duplicate detection +DEFAULT_MATCH_FIELDS = ['title', 'login', 'password'] +ENTERPRISE_FIELD_KEYS = ['title', 'url', 'record_type'] + +# Report field names +FIELD_TITLE = 'Title' +FIELD_LOGIN = 'Login' +FIELD_PASSWORD = 'Password' +FIELD_WEBSITE_ADDRESS = 'Website Address' +FIELD_CUSTOM_FIELDS = 'Custom Fields' +FIELD_SHARES = 'Shares' +FIELD_RECORD_UID = 'record_uid' +FIELD_GROUP = 'group' +FIELD_URL = 'url' +FIELD_RECORD_OWNER = 'record_owner' +FIELD_SHARED_TO = 'shared_to' +FIELD_SHARED_FOLDER_UID = 'shared_folder_uid' + +# Report titles +ENTERPRISE_DUPLICATE_TITLE = 'Duplicate Search Results (Enterprise Scope):' +VAULT_DUPLICATE_TITLE = 'Duplicates Found:' +NO_DUPLICATES_FOUND = 'No duplicates found.' + class ClipboardCommand(base.ArgparseCommand): """Command to copy record data to clipboard or output to various destinations.""" @@ -700,3 +732,377 @@ def _execute_restore_request(self, vault: vault_online.VaultOnline, record_uid: if ros and ros.status != record_pb2.RS_SUCCESS: raise base.CommandError(f'Failed to restore record "{record_uid}": {ros.message}') + +class FindDuplicateCommand(base.ArgparseCommand): + """ + Command to find and optionally merge duplicate records in a vault. + + This command can identify duplicates based on various field combinations + (title, login, password, URL, custom fields, shares) and optionally + consolidate them by removing the duplicate entries. + """ + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='find-duplicates', + description='List duplicated records.', + parents=[base.report_output_parser] + ) + self.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--title', dest='title', action='store_true', help='Match duplicates by title.') + parser.add_argument('--login', dest='login', action='store_true', help='Match duplicates by login.') + parser.add_argument('--password', dest='password', action='store_true', help='Match duplicates by password.') + parser.add_argument('--url', dest='url', action='store_true', help='Match duplicates by URL.') + parser.add_argument('--shares', action='store_true', help='Match duplicates by share permissions') + parser.add_argument('--full', dest='full', action='store_true', help='Match duplicates by all fields.') + merge_help = 'Consolidate duplicate records (matched by all fields, including shares)' + parser.add_argument('-m', '--merge', action='store_true', help=merge_help) + ignore_shares_txt = 'ignore share permissions when grouping duplicate records to merge' + parser.add_argument('--ignore-shares-on-merge', action='store_true', help=ignore_shares_txt) + force_help = 'Delete duplicates w/o being prompted for confirmation (valid only w/ --merge option)' + parser.add_argument('-f', '--force', action='store_true', help=force_help) + dry_run_help = 'Simulate removing duplicates (with this flog, no records are ever removed or modified). ' \ + 'Valid only w/ --merge flag' + parser.add_argument('-n', '--dry-run', action='store_true', help=dry_run_help) + parser.add_argument('-q', '--quiet', action='store_true', + help='Suppress screen output, valid only w/ --force flag') + scope_help = 'The scope of the search (limited to current vault if not specified)' + parser.add_argument('-s', '--scope', action='store', choices=['vault', 'enterprise'], default='vault', + help=scope_help) + refresh_help = 'Populate local cache with latest compliance data . Valid only w/ --scope=enterprise option.' + parser.add_argument('-r', '--refresh-data', action='store_true', help=refresh_help) + + def execute(self, context: KeeperParams, **kwargs): + self._validate_context(context) + + scope = kwargs.get('scope', 'vault') + if scope == 'enterprise': + raise base.CommandError('Enterprise scope not yet implemented') + + return self._process_vault_duplicates(context, kwargs) + + def _validate_context(self, context: KeeperParams): + if not context.vault: + raise base.CommandError('Vault is not initialized') + + def _process_vault_duplicates(self, context: KeeperParams, kwargs: dict): + vault = context.vault + match_fields = self._determine_match_fields(kwargs) + + hashes = self._build_duplicate_hashes(vault, match_fields) + + fields = self._build_field_list(match_fields) + logging_fn = self._get_logging_function(kwargs) + logging_fn('Find duplicated records by: %s', ', '.join(fields)) + + partitions = [rec_uids for rec_uids in hashes.values() if len(rec_uids) > 1] + + if not partitions: + logging_fn(NO_DUPLICATES_FOUND) + return + + partitions = self._apply_share_partitioning(context, partitions, match_fields) + + if not partitions: + logging_fn(NO_DUPLICATES_FOUND) + return + + return self._generate_duplicate_report(context, partitions, match_fields, kwargs) + + def _get_logging_function(self, kwargs): + quiet = kwargs.get('quiet', False) + dry_run = kwargs.get('dry_run', False) + quiet = quiet and not dry_run + return logger.info if not quiet else logger.debug + + def _build_duplicate_hashes(self, vault, match_fields): + hashes = {} + for record_uid in vault.vault_data._records: + record = vault.vault_data.load_record(record_uid) + if not record or not isinstance(record, (vault_record.PasswordRecord, vault_record.TypedRecord)): + continue + + hash_value, non_empty = self._create_record_hash(record, match_fields) + + if non_empty > 0: + rec_uids = hashes.get(hash_value, set()) + rec_uids.add(record_uid) + hashes[hash_value] = rec_uids + return hashes + + def _apply_share_partitioning(self, context, partitions, match_fields): + if not match_fields['by_shares']: + return partitions + + r_uids = [rec_uid for duplicates in partitions for rec_uid in duplicates] + shared_records_lookup = share_utils.get_shared_records(context, r_uids, cache_only=True) + + return self._partition_by_shares(partitions, shared_records_lookup) + + def _partition_by_shares(self, duplicate_sets, shared_recs_lookup): + result = [] + for duplicates in duplicate_sets: + recs_by_hash = {} + for rec_uid in duplicates: + shared_rec = shared_recs_lookup.get(rec_uid) + permissions = shared_rec.permissions + + permissions = self._filter_team_user_permissions(permissions) + permissions = {k: p for k, p in permissions.items() if p.to_name != shared_rec.owner} + + permissions_keys = list(permissions.keys()) + permissions_keys.sort() + + to_hash = ';'.join(f'{k}={permissions.get(k).permissions_text}' for k in permissions_keys) + to_hash = to_hash or NON_SHARED_LABEL + + h = hashlib.sha256() + h.update(to_hash.encode()) + h_val = h.hexdigest() + + r_uids = recs_by_hash.get(h_val, set()) + r_uids.add(rec_uid) + recs_by_hash[h_val] = r_uids + + result.extend([r for r in recs_by_hash.values() if len(r) > 1]) + + return result + + def _generate_duplicate_report(self, context, partitions, match_fields, kwargs): + vault = context.vault + out_fmt = kwargs.get('format', 'table') + out_dst = kwargs.get('output') + + headers = self._build_report_headers(match_fields, out_fmt) + + table, table_raw, to_remove = self._build_report_data(context, vault, partitions, match_fields) + + if match_fields['consolidate']: + return self._consolidate_duplicates(vault, headers, table_raw, to_remove, kwargs) + else: + title = VAULT_DUPLICATE_TITLE + return report_utils.dump_report_data(table, headers, title=title, fmt=out_fmt, filename=out_dst, group_by=0) + + def _build_report_headers(self, match_fields, out_fmt): + headers = [FIELD_GROUP, 'title', 'login'] + if match_fields['by_url']: + headers.append(FIELD_URL) + headers.extend(['uid', FIELD_RECORD_OWNER, FIELD_SHARED_TO]) + return [report_utils.field_to_title(h) for h in headers] if out_fmt != 'json' else headers + + def _build_report_data(self, context, vault, partitions, match_fields): + shared_records_lookup = share_utils.get_shared_records( + context, + [rec_uid for duplicates in partitions for rec_uid in duplicates], + cache_only=True + ) + + table = [] + table_raw = [] + to_remove = set() + + for i, partition in enumerate(partitions): + for j, record_uid in enumerate(partition): + row = self._build_report_row(vault, shared_records_lookup, i, record_uid, match_fields) + table.append(row) + + if j != 0: # Mark for removal (all except first in each partition) + to_remove.add(record_uid) + table_raw.append(row) + + return table, table_raw, to_remove + + def _build_report_row(self, vault, shared_records_lookup, group_index, record_uid, match_fields): + record = vault.vault_data.load_record(record_uid) + shared_record = shared_records_lookup[record_uid] + + owner = self._get_record_owner(vault, record_uid) + title, login, url = self._extract_record_info(record, match_fields) + url = self._format_url(url, match_fields['by_url']) + shares = self._extract_share_info(shared_record, owner) + + return [group_index + 1, title, login] + url + [record_uid, owner, shares] + + def _get_record_owner(self, vault, record_uid): + record_details = vault.vault_data.get_record(record_uid) + return record_details.flags.IsOwner + + def _extract_share_info(self, shared_record, owner): + perms = {k: p for k, p in shared_record.permissions.items()} + keys = list(perms.keys()) + keys.sort() + perms = [perms.get(k) for k in keys] + perms = [p for p in perms if TEAM_USER_TYPE not in p.types or len(p.types) > 1] + return '\n'.join([p.to_name for p in perms if owner != p.to_name]) + + def _consolidate_duplicates(self, vault, headers, table_raw, to_remove, kwargs): + """Consolidate (remove) duplicate records.""" + uid_header = report_utils.field_to_title('uid') + record_uid_index = headers.index(uid_header) if uid_header in headers else None + + if not record_uid_index: + raise base.CommandError('Cannot find record UID for duplicate record') + + dup_info = [r for r in table_raw for rec_uid in to_remove if r[record_uid_index] == rec_uid] + return self._remove_duplicates(vault, dup_info, headers, to_remove, kwargs) + + def _remove_duplicates(self, vault, dupe_info, col_headers, dupe_uids, kwargs): + """Remove duplicate records with confirmation.""" + def confirm_removal(cols): + prompt_title = f'\nThe following duplicate {"records have" if len(dupe_uids) > 1 else "record has"}' \ + f' been marked for removal:\n' + indices = (idx + 1 for idx in range(len(dupe_info))) + prompt_report = prompt_title + '\n' + report_utils.tabulate(dupe_info, col_headers, showindex=indices) + prompt_msg = prompt_report + '\n\nDo you wish to proceed?' + return prompt_utils.user_choice(prompt_msg, 'yn', default='n') in ('y', 'yes') + + if kwargs.get('force') or confirm_removal(col_headers): + record_management.delete_vault_objects(vault, list(dupe_uids)) + + def _determine_match_fields(self, kwargs): + by_title = kwargs.get('title', False) + by_login = kwargs.get('login', False) + by_password = kwargs.get('password', False) + by_url = kwargs.get('url', False) + by_custom = kwargs.get('full', False) + by_shares = kwargs.get('shares', False) + consolidate = kwargs.get('merge', False) + + if consolidate or by_custom: + by_title = True + by_login = True + by_password = True + by_url = True + by_shares = not kwargs.get('ignore_shares_on_merge') if consolidate else True + elif not any([by_title, by_login, by_password, by_url]): + by_title = True + by_login = True + by_password = True + + return { + 'by_title': by_title, + 'by_login': by_login, + 'by_password': by_password, + 'by_url': by_url, + 'by_custom': consolidate or by_custom, + 'by_shares': by_shares, + 'consolidate': consolidate + } + + def _build_field_list(self, match_fields): + fields = [] + if match_fields['by_title']: + fields.append(FIELD_TITLE) + if match_fields['by_login']: + fields.append(FIELD_LOGIN) + if match_fields['by_password']: + fields.append(FIELD_PASSWORD) + if match_fields['by_url']: + fields.append(FIELD_WEBSITE_ADDRESS) + if match_fields['by_custom']: + fields.append(FIELD_CUSTOM_FIELDS) + if match_fields['by_shares']: + fields.append(FIELD_SHARES) + return fields + + def _filter_team_user_permissions(self, permissions): + filtered_perms = { + k: p for k, p in permissions.items() + if TEAM_USER_TYPE not in p.types or len(p.types) > 1 + } + return filtered_perms + + def _create_record_hash(self, record, match_fields): + tokens = [] + + if match_fields['by_title']: + tokens.append((record.title or '').lower()) + + if match_fields['by_login']: + if isinstance(record, vault_record.PasswordRecord): + tokens.append((record.login or '').lower()) + elif isinstance(record, vault_record.TypedRecord): + login_field = record.get_typed_field('login') + if login_field: + tokens.append((login_field.get_default_value(str) or '').lower()) + + if match_fields['by_password']: + tokens.append(record.extract_password() or '') + + if match_fields['by_url']: + tokens.append(record.extract_url() or '') + + hasher = hashlib.sha256() + non_empty = 0 + + for token in tokens: + if token: + non_empty += 1 + hasher.update(token.encode()) + + if match_fields['by_custom'] and isinstance(record, vault_record.TypedRecord): + non_empty += self._hash_custom_fields(record, hasher) + + return hasher.hexdigest(), non_empty + + def _hash_custom_fields(self, record, hasher): + customs = {} + non_empty = 0 + + for field in record.custom: + name = field.label if field.label != '' else field.type + value = field.value + + if not name or not value: + continue + + if isinstance(value, list): + value = '|'.join(sorted(str(x) for x in value)) + elif isinstance(value, int): + value = str(value) if value != 0 else None + elif isinstance(value, dict): + keys = sorted(value.keys()) + value = ';'.join(f'{k}:{value[k]}' for k in keys if value.get(k)) + elif not isinstance(value, str): + value = None + + if value: + customs[name] = value + + if record.get_typed_field('totp'): + customs['totp'] = record.get_typed_field('totp').get_default_value(str) + + if record.record_type: + customs['type:'] = record.record_type + + for key in sorted(customs.keys()): + non_empty += 1 + for_hash = f'{key}={customs[key]}' + hasher.update(for_hash.encode('utf-8')) + + return non_empty + + def _extract_record_info(self, record, match_fields): + title = record.title or '' + + if isinstance(record, vault_record.PasswordRecord): + url = record.link or '' + login = record.login or '' + elif isinstance(record, vault_record.TypedRecord): + login = record.get_typed_field('login').get_default_value(str) or '' + url = record.extract_url() or '' + else: + login = '' + url = '' + + return title, login, url + + def _format_url(self, url, include_in_output): + parsed_url = urllib.parse.urlparse(url).hostname + parsed_url = parsed_url[:URL_DISPLAY_LENGTH] if parsed_url else '' + return [parsed_url] if include_in_output else [] + diff --git a/keepercli-package/src/keepercli/commands/share_management.py b/keepercli-package/src/keepercli/commands/share_management.py index cd47451d..4843b8da 100644 --- a/keepercli-package/src/keepercli/commands/share_management.py +++ b/keepercli-package/src/keepercli/commands/share_management.py @@ -38,9 +38,23 @@ class ManagePermission(Enum): logger = api.get_logger() + TIMESTAMP_MILLISECONDS_FACTOR = 1000 TRUNCATE_SUFFIX = '...' +# Constants for FindDuplicatesCommand +URL_TRUNCATE_LENGTH = 30 +NON_SHARED_DEFAULT = 'non-shared' +CUSTOM_FIELD_TYPE_PREFIX = 'type:' +TOTP_FIELD_NAME = 'totp' +LIST_SEPARATOR = '|' +DICT_SEPARATOR = ';' +KEY_VALUE_SEPARATOR = '=' +PERMISSION_SEPARATOR = '=' +SHARE_NAMES_SEPARATOR = ', ' +SUPPORTED_RECORD_VERSIONS = {2, 3} +DEFAULT_SEARCH_FIELDS = ['by_title', 'by_login', 'by_password'] + def set_expiration_fields(obj, expiration): """Set expiration and timerNotificationType fields on proto object if expiration is provided.""" if isinstance(expiration, int): @@ -130,10 +144,14 @@ def execute(self, context: KeeperParams, **kwargs) -> None: shared_objects = share_utils.get_share_objects(vault=vault) known_users = shared_objects.get('users', {}) known_emails = [u.casefold() for u in known_users.keys()] - is_unknown = lambda e: e.casefold() not in known_emails and utils.is_email(e) + def is_unknown(e): + return e.casefold() not in known_emails and utils.is_email(e) unknowns = [e for e in emails if is_unknown(e)] if unknowns: - username_map = {e: ShareRecordCommand.get_contact(e, known_users) for e in unknowns} + username_map = { + e: ShareRecordCommand.get_contact(e, known_users) + for e in unknowns + } table = [[k, v] for k, v in username_map.items()] logger.info(f'{len(unknowns)} unrecognized share recipient(s) and closest matching contact(s)') report_utils.dump_report_data(table, ['Username', 'From Contacts']) @@ -364,7 +382,7 @@ def prep_request(context: KeeperParams, data = json.loads(rec['data_unencrypted'].decode()) if isinstance(data, dict) and 'title' in data: record_titles[record_uid] = data['title'] - except Exception: + except (ValueError, AttributeError): pass record_path = share_utils.resolve_record_share_path(context=context, record_uid=record_uid) @@ -604,8 +622,8 @@ def get_share_admin_obj_uids(vault: vault_online.VaultOnline, obj_names, obj_typ sa_obj_uids = {sa_obj.uid for sa_obj in rs.isObjectShareAdmin if sa_obj.isAdmin} sa_obj_uids = {utils.base64_url_encode(uid) for uid in sa_obj_uids} return sa_obj_uids - except Exception as e: - raise ValueError(f'get_share_admin: msg = {e}') + except (ValueError, AttributeError) as e: + raise ValueError(f'get_share_admin: msg = {e}') from e def get_record_uids(context: KeeperParams, name: str) -> set[str]: """Get record UIDs by name or UID.""" @@ -639,7 +657,8 @@ def get_record_uids(context: KeeperParams, name: str) -> set[str]: if all_folders: shared_folder_uids.update(shared_folder_cache.keys()) else: - get_folder_by_uid = lambda uid: folder_cache.get(uid) + def get_folder_by_uid(uid): + return folder_cache.get(uid) folder_uids = { uid for name in names if name @@ -1380,4 +1399,4 @@ def _remove_share(self, vault, record_uid: str, client_id: bytes, share_name: st rq.clients.append(client_id) vault.keeper_auth.execute_auth_rest(request=rq, rest_endpoint=ApiUrl.REMOVE_EXTERNAL_SHARE.value) - logger.info('One-time share \"%s\" is removed from record \"%s\"', share_name, record_name) \ No newline at end of file + logger.info('One-time share \"%s\" is removed from record \"%s\"', share_name, record_name) diff --git a/keepercli-package/src/keepercli/helpers/share_record.py b/keepercli-package/src/keepercli/helpers/share_record.py new file mode 100644 index 00000000..4da213f2 --- /dev/null +++ b/keepercli-package/src/keepercli/helpers/share_record.py @@ -0,0 +1,85 @@ +from enum import Enum +from typing import Dict + +from keepersdk.vault import vault_online, vault_record + +from .. import api +from ..params import KeeperParams +logger = api.get_logger() + +TEXT_EDIT = 'Edit' +TEXT_SHARE = 'Share' + +class SharePermissions: + SharePermissionsType = Enum('SharePermissionsType', ['USER', 'SF_USER', 'TEAM', 'TEAM_USER']) + bits_text_lookup = {(1 << 0): TEXT_EDIT, (1 << 1): TEXT_SHARE} + + def __init__(self, sp_types=None, to_name='', permissions_text='', types=None): + self.to_uid = '' + self.to_name = to_name + self.can_edit = False + self.can_share = False + self.can_view = True + self.expiration = 0 + self.folder_path = '' + self.types = set() + self.bits = 0 + self.is_admin = False + self.team_members = dict() + self.user_perms: Dict[str, 'SharePermissions'] = {} + self.team_perms: Dict[str, 'SharePermissions'] = {} + self.permissions_text = permissions_text + + if types is not None: + if isinstance(types, list): + self.types.update(types) + else: + self.types.add(types) + + self.update_types(sp_types) + + def update_types(self, sp_types): + if sp_types is not None: + update_types_fn = self.types.update if isinstance(sp_types, set) else self.types.add + update_types_fn(sp_types) + + +class SharedRecord: + """Defines a Keeper Shared Record (shared either via Direct-Share or as a child of a Shared-Folder node)""" + + def __init__(self, context: KeeperParams, record: vault_record.KeeperRecordInfo, sf_sharing_admins=None, team_members=None, role_restricted_members=None): + """Initialize SharedRecord with proper error handling.""" + try: + self.context = context + self.record = record + self.uid = record.record_uid + + self.name = record.title + self.shared_folders = None + self.sf_shares = {} + self.permissions: Dict[str, SharePermissions] = {} + self.team_permissions: Dict[str, SharePermissions] = {} + self.user_permissions: Dict[str, SharePermissions] = {} + self.revision = None + self.folder_uids = [] + self.folder_paths = [] + + self._initialize_folder_info(context.vault) + self.team_members = team_members or {} + + if sf_sharing_admins is None: + sf_sharing_admins = {} + if role_restricted_members is None: + role_restricted_members = set() + + except Exception as e: + logger.error(f"Failed to initialize SharedRecord: {e}") + + def _initialize_folder_info(self, vault: vault_online.VaultOnline): + """Initialize folder information for the record.""" + try: + from keepersdk.vault import vault_utils + folders = vault_utils.get_folders_for_record(vault.vault_data, self.uid) + self.folder_uids = [f.folder_uid for f in folders] + except Exception as e: + logger.debug(f"Failed to initialize folder info: {e}") diff --git a/keepercli-package/src/keepercli/helpers/share_utils.py b/keepercli-package/src/keepercli/helpers/share_utils.py index 3eb2c2ca..498c7b98 100644 --- a/keepercli-package/src/keepercli/helpers/share_utils.py +++ b/keepercli-package/src/keepercli/helpers/share_utils.py @@ -4,35 +4,44 @@ from keepersdk import crypto, utils from keepersdk.proto import enterprise_pb2, record_pb2 -from keepersdk.vault import storage_types, vault_online, vault_record, vault_utils +from keepersdk.vault import vault_online, vault_record, vault_utils from .. import api from ..commands import enterprise_utils from ..helpers import timeout_utils, folder_utils from ..params import KeeperParams -# Constants +# API Endpoints RECORD_DETAILS_URL = 'vault/get_records_details' SHARE_OBJECTS_API = 'vault/get_share_objects' TEAM_MEMBERS_ENDPOINT = 'vault/get_team_members' SHARING_ADMINS_ENDPOINT = 'enterprise/get_sharing_admins' +SHARE_ADMIN_API = 'vault/am_i_share_admin' +SHARE_UPDATE_API = 'vault/records_share_update' +SHARE_FOLDER_UPDATE_API = 'vault/shared_folder_update_v3' +REMOVE_EXTERNAL_SHARE_API = 'vault/external_share_remove' -# Record processing constants +# Record Processing Constants CHUNK_SIZE = 999 +MAX_BATCH_SIZE = 990 RECORD_KEY_LENGTH_V2 = 60 DEFAULT_EXPIRATION = 0 NEVER_EXPIRES = -1 NEVER_EXPIRES_STRING = 'never' +TIMESTAMP_MILLISECONDS_FACTOR = 1000 +TRUNCATE_SUFFIX = '...' +TRUNCATE_LENGTH = 20 -# Record version constants +# Record Version Constants MAX_V2_VERSION = 2 V3_VERSION = 3 V4_VERSION = 4 -# User type constants +# User Type Constants TEAM_USER_TYPE = 2 +USER_TYPE_INACTIVE = 2 -# Permission field names +# Permission Field Names CAN_SHARE_PERMISSION = 'can_share' CAN_EDIT_FIELD = 'can_edit' CAN_SHARE_FIELD = 'can_share' @@ -41,16 +50,13 @@ SHARED_FOLDER_UID_FIELD = 'shared_folder_uid' TEAM_UID_FIELD = 'team_uid' -# Share object categories +# Share Object Categories RELATIONSHIP_CATEGORY = 'relationship' FAMILY_CATEGORY = 'family' ENTERPRISE_CATEGORY = 'enterprise' MC_CATEGORY = 'mc' -# Default empty dictionaries -EMPTY_SHARE_OBJECTS = {'users': {}, 'enterprises': {}, 'teams': {}} - -# Record field names +# Record Field Names TITLE_FIELD = 'title' NAME_FIELD = 'name' IS_SA_FIELD = 'is_sa' @@ -61,85 +67,210 @@ USER_PERMISSIONS_FIELD = 'user_permissions' SHARED_FOLDER_PERMISSIONS_FIELD = 'shared_folder_permissions' +# Key Constants for Data Access +KEY_USERNAME = 'username' +KEY_TEAM_UID = 'team_uid' +KEY_RECORD_UID = 'record_uid' +KEY_SHARED_FOLDER_UID = 'shared_folder_uid' +KEY_USER_PERMISSIONS = 'user_permissions' +KEY_TEAM_PERMISSIONS = 'team_permissions' +KEY_SHARED_FOLDER_PERMISSIONS = 'shared_folder_permissions' +KEY_SHARES = 'shares' +KEY_UID = 'uid' +KEY_NAME = 'name' +KEY_EDITABLE = 'editable' +KEY_SHAREABLE = 'shareable' +KEY_MANAGE_RECORDS = 'manage_records' +KEY_MANAGE_USERS = 'manage_users' +KEY_SHARE_ADMIN = 'share_admin' +KEY_IS_ADMIN = 'is_admin' +KEY_EXPIRATION = 'expiration' +KEY_OWNER = 'owner' +KEY_VIEW = 'view' +KEY_TITLE = 'title' + +# Enterprise Keys +KEY_ENTERPRISE = 'enterprise' +KEY_ENTERPRISE_USER_ID = 'enterprise_user_id' +KEY_USER_TYPE = 'user_type' +KEY_ROLE_ID = 'role_id' +KEY_ROLE_ENFORCEMENTS = 'role_enforcements' +KEY_ROLE_USERS = 'role_users' +KEY_ROLE_TEAMS = 'role_teams' +KEY_TEAM_USERS = 'team_users' +KEY_USERS = 'users' +KEY_TEAMS = 'teams' +KEY_ENFORCEMENTS = 'enforcements' + +# Vault Keys +KEY_VAULT = 'vault' +KEY_VAULT_DATA = 'vault_data' +KEY_SHARED_FOLDER_CACHE = 'shared_folder_cache' +KEY_RECORD_CACHE = 'record_cache' +KEY_RECORD_OWNER_CACHE = 'record_owner_cache' + +# Restriction Keys +KEY_RESTRICT_EDIT = 'restrict_edit' +KEY_RESTRICT_SHARING = 'restrict_sharing' +KEY_RESTRICT_VIEW = 'restrict_view' +KEY_RESTRICT_SHARING_ALL = 'restrict_sharing_all' + +# Permission Constants +PERMISSION_EDIT = 'edit' +PERMISSION_SHARE = 'share' +PERMISSION_VIEW = 'view' + +# Text Constants +TEXT_EDIT = 'Edit' +TEXT_SHARE = 'Share' +TEXT_READ_ONLY = 'Read Only' +TEXT_LAUNCH_ONLY = 'Launch Only' +TEXT_CAN_PREFIX = 'Can ' +TEXT_TEAM_PREFIX = '(Team)' +TEXT_TEAM_USER_PREFIX = '(Team User)' + +# Default Values +EMPTY_SHARE_OBJECTS = {'users': {}, 'enterprises': {}, 'teams': {}} + +# Time Constants +SIX_MONTHS_IN_SECONDS = 182 * 24 * 60 * 60 + +# Status Messages +STATUS_SUCCESS = 'success' +STATUS_INVITED = 'invited' +STATUS_EXPIRED = 'Expired' +STATUS_OPENED = 'Opened' +STATUS_GENERATED = 'Generated' + logger = api.get_logger() +class ShareManagementError(Exception): + """Base exception for share management operations.""" + pass + + +class ShareValidationError(ShareManagementError): + """Raised when share validation fails.""" + pass + + +class ShareNotFoundError(ShareManagementError): + """Raised when a share or record is not found.""" + pass + + def get_share_expiration(expire_at: Optional[str], expire_in: Optional[str]) -> int: + """ + Calculate share expiration timestamp from expire_at or expire_in parameters. + + Args: + expire_at: ISO datetime string or 'never' + expire_in: Time period string or 'never' + + Returns: + Unix timestamp for expiration + + Raises: + ShareValidationError: If expiration format is invalid + """ if not expire_at and not expire_in: return DEFAULT_EXPIRATION - dt = None - if isinstance(expire_at, str): - if expire_at == NEVER_EXPIRES_STRING: - return NEVER_EXPIRES - dt = datetime.datetime.fromisoformat(expire_at) - elif isinstance(expire_in, str): - if expire_in == NEVER_EXPIRES_STRING: - return NEVER_EXPIRES - td = timeout_utils.parse_timeout(expire_in) - dt = datetime.datetime.now() + td - if dt is None: - raise ValueError(f'Incorrect expiration: {expire_at or expire_in}') + try: + dt = None + if isinstance(expire_at, str): + if expire_at == NEVER_EXPIRES_STRING: + return NEVER_EXPIRES + dt = datetime.datetime.fromisoformat(expire_at) + elif isinstance(expire_in, str): + if expire_in == NEVER_EXPIRES_STRING: + return NEVER_EXPIRES + td = timeout_utils.parse_timeout(expire_in) + dt = datetime.datetime.now() + td + + if dt is None: + raise ShareValidationError(f'Incorrect expiration: {expire_at or expire_in}') - return int(dt.timestamp()) + return int(dt.timestamp()) + except Exception as e: + if isinstance(e, ShareValidationError): + raise + raise ShareValidationError(f'Invalid expiration format: {e}') from e def get_share_objects(vault: vault_online.VaultOnline) -> Dict[str, Dict[str, Any]]: - request = record_pb2.GetShareObjectsRequest() - - response = vault.keeper_auth.execute_auth_rest( - rest_endpoint=SHARE_OBJECTS_API, - request=request, - response_type=record_pb2.GetShareObjectsResponse - ) - - if not response: - return EMPTY_SHARE_OBJECTS - - users_by_type = { - RELATIONSHIP_CATEGORY: response.shareRelationships, - FAMILY_CATEGORY: response.shareFamilyUsers, - ENTERPRISE_CATEGORY: response.shareEnterpriseUsers, - MC_CATEGORY: response.shareMCEnterpriseUsers, - } + """ + Retrieve share objects (users, enterprises, teams) from the vault. - def process_users(users_data: Iterable[Any], category: str) -> Dict[str, Dict[str, Any]]: - """Process user data and add category information.""" - return { - user.username: { - NAME_FIELD: user.fullname, - IS_SA_FIELD: user.isShareAdmin, - ENTERPRISE_ID_FIELD: user.enterpriseId, - STATUS_FIELD: user.status, - CATEGORY_FIELD: category - } for user in users_data + Args: + vault: VaultOnline instance + + Returns: + Dictionary containing users, enterprises, and teams + """ + try: + request = record_pb2.GetShareObjectsRequest() + + response = vault.keeper_auth.execute_auth_rest( + rest_endpoint=SHARE_OBJECTS_API, + request=request, + response_type=record_pb2.GetShareObjectsResponse + ) + + if not response: + return EMPTY_SHARE_OBJECTS + + users_by_type = { + RELATIONSHIP_CATEGORY: response.shareRelationships, + FAMILY_CATEGORY: response.shareFamilyUsers, + ENTERPRISE_CATEGORY: response.shareEnterpriseUsers, + MC_CATEGORY: response.shareMCEnterpriseUsers, } - - users = {} - for category, users_data in users_by_type.items(): - users.update(process_users(users_data, category)) - - enterprises = { - str(enterprise.enterpriseId): enterprise.enterprisename - for enterprise in response.shareEnterpriseNames - } - - def process_teams(teams_data: Iterable[Any]) -> Dict[str, Dict[str, Any]]: + + users = {} + for category, users_data in users_by_type.items(): + users.update(_process_users(users_data, category)) + + enterprises = { + str(enterprise.enterpriseId): enterprise.enterprisename + for enterprise in response.shareEnterpriseNames + } + + teams = _process_teams(response.shareTeams) + teams_mc = _process_teams(response.shareMCTeams) + return { - utils.base64_url_encode(team.teamUid): { - NAME_FIELD: team.teamname, - ENTERPRISE_ID_FIELD: team.enterpriseId - } for team in teams_data + 'users': users, + 'enterprises': enterprises, + 'teams': {**teams, **teams_mc} } - - teams = process_teams(response.shareTeams) - teams_mc = process_teams(response.shareMCTeams) - + except Exception as e: + logger.error(f"Failed to get share objects: {e}") + return EMPTY_SHARE_OBJECTS + + +def _process_users(users_data: Iterable[Any], category: str) -> Dict[str, Dict[str, Any]]: + """Process user data and add category information.""" return { - 'users': users, - 'enterprises': enterprises, - 'teams': {**teams, **teams_mc} + user.username: { + NAME_FIELD: user.fullname, + IS_SA_FIELD: user.isShareAdmin, + ENTERPRISE_ID_FIELD: user.enterpriseId, + STATUS_FIELD: user.status, + CATEGORY_FIELD: category + } for user in users_data + } + + +def _process_teams(teams_data: Iterable[Any]) -> Dict[str, Dict[str, Any]]: + """Process team data.""" + return { + utils.base64_url_encode(team.teamUid): { + NAME_FIELD: team.teamname, + ENTERPRISE_ID_FIELD: team.enterpriseId + } for team in teams_data } @@ -148,18 +279,56 @@ def load_records_in_shared_folder( shared_folder_uid: str, record_uids: Optional[set[str]] = None ) -> None: - shared_folder = None + """ + Load records from a shared folder into the vault. + + Args: + vault: VaultOnline instance + shared_folder_uid: UID of the shared folder + record_uids: Optional set of specific record UIDs to load + + Raises: + ShareNotFoundError: If shared folder is not found + ShareManagementError: If loading fails + """ + try: + shared_folder = _find_shared_folder(vault, shared_folder_uid) + if not shared_folder: + raise ShareNotFoundError(f'Shared folder "{shared_folder_uid}" is not loaded.') + + shared_folder_key = vault.vault_data._shared_folders[shared_folder_uid].shared_folder_key + record_keys = _decrypt_record_keys(vault, shared_folder, shared_folder_key) + + record_cache = [x.record_uid for x in vault.vault_data.records()] + + if record_uids: + record_set = set(record_uids) + record_set.intersection_update(record_keys.keys()) + else: + record_set = set(record_keys.keys()) + record_set.difference_update(record_cache) + + _load_records_in_batches(vault, record_set, record_keys) + + except ShareNotFoundError: + raise + except Exception as e: + raise ShareManagementError(f"Failed to load records in shared folder: {e}") from e + + +def _find_shared_folder(vault: vault_online.VaultOnline, shared_folder_uid: str): + """Find shared folder by UID.""" for shared_folder_info in vault.vault_data.shared_folders(): if shared_folder_uid == shared_folder_info.shared_folder_uid: - shared_folder = vault.vault_data.load_shared_folder(shared_folder_uid=shared_folder_uid) - break - - if not shared_folder: - raise Exception(f'Shared folder "{shared_folder_uid}" is not loaded.') - - shared_folder_key = vault.vault_data._shared_folders[shared_folder_uid].shared_folder_key + return vault.vault_data.load_shared_folder(shared_folder_uid=shared_folder_uid) + return None + + +def _decrypt_record_keys(vault: vault_online.VaultOnline, shared_folder, shared_folder_key): + """Decrypt record keys for shared folder.""" record_keys = {} sf_record_keys = vault.vault_data.storage.record_keys.get_links_by_object(shared_folder.shared_folder_uid) or [] + for rk in sf_record_keys: record_uid = getattr(rk, 'record_uid', None) try: @@ -175,17 +344,12 @@ def load_records_in_shared_folder( record_keys[record_uid] = record_key except Exception as e: logger.error(f'Cannot decrypt record "{record_uid}" key: {e}') + + return record_keys - record_cache = [x.record_uid for x in vault.vault_data.records()] - - if record_uids: - record_set = set(record_uids) - record_set.intersection_update(record_keys.keys()) - else: - record_set = set(record_keys.keys()) - record_set.difference_update(record_cache) - # Load records in batches +def _load_records_in_batches(vault: vault_online.VaultOnline, record_set: set, record_keys: dict): + """Load records in batches to avoid API limits.""" while len(record_set) > 0: rq = record_pb2.GetRecordDataWithAccessInfoRequest() rq.clientTime = utils.current_milli_time() @@ -198,93 +362,112 @@ def load_records_in_shared_folder( logger.debug('Incorrect record UID "%s": %s', uid, e) record_set.clear() - rs = vault.keeper_auth.execute_auth_rest( + response = vault.keeper_auth.execute_auth_rest( rest_endpoint=RECORD_DETAILS_URL, request=rq, response_type=record_pb2.GetRecordDataWithAccessInfoResponse ) - if not rs or not rs.recordDataWithAccessInfo: + if not response or not response.recordDataWithAccessInfo: logger.warning("No record data received from API") break - for record_info in rs.recordDataWithAccessInfo: - record_uid = utils.base64_url_encode(record_info.recordUid) - record_data = record_info.recordData - try: - if record_data.recordUid and record_data.recordKey: - owner_id = utils.base64_url_encode(record_data.recordUid) - if owner_id in record_keys: - record_keys[record_uid] = crypto.decrypt_aes_v2(record_data.recordKey, record_keys[owner_id]) - - if record_uid not in record_keys: - continue - - record_key = record_keys[record_uid] - version = record_data.version - record = { - 'record_uid': record_uid, - 'revision': record_data.revision, - 'version': version, - 'shared': record_data.shared, - 'data': record_data.encryptedRecordData, - 'record_key_unencrypted': record_keys[record_uid], - 'client_modified_time': record_data.clientModifiedTime, - } - data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) - if version <= MAX_V2_VERSION: - record['data_unencrypted'] = crypto.decrypt_aes_v1(data_decoded, record_key) - else: - record['data_unencrypted'] = crypto.decrypt_aes_v2(data_decoded, record_key) - - # Handle extra data for v2 records - if record_data.encryptedExtraData and version <= MAX_V2_VERSION: - record['extra'] = record_data.encryptedExtraData - extra_decoded = utils.base64_url_decode(record_data.encryptedExtraData) - record['extra_unencrypted'] = crypto.decrypt_aes_v1(extra_decoded, record_key) - - # Handle v3 typed records with references - if version == V3_VERSION: - v3_record = vault.vault_data.load_record(record_uid=record_uid) - if isinstance(v3_record, vault_record.TypedRecord): - for ref in itertools.chain(v3_record.fields, v3_record.custom): - if ref.type.endswith('Ref') and isinstance(ref.value, list): - record_set.update(ref.value) - - # Handle v4 records with file attachments - elif version == V4_VERSION: - if record_data.fileSize > 0: - record['file_size'] = record_data.fileSize - if record_data.thumbnailSize > 0: - record['thumbnail_size'] = record_data.thumbnailSize - - # Handle linked record metadata - if record_data.recordUid and record_data.recordKey: - record['owner_uid'] = utils.base64_url_encode(record_data.recordUid) - record['link_key'] = utils.base64_url_encode(record_data.recordKey) - - # Add share permissions - record['shares'] = { - 'user_permissions': [{ - 'username': up.username, - 'owner': up.owner, - 'share_admin': up.shareAdmin, - 'shareable': up.sharable, - 'editable': up.editable, - 'awaiting_approval': up.awaitingApproval, - 'expiration': up.expiration, - } for up in record_info.userPermission], - 'shared_folder_permissions': [{ - 'shared_folder_uid': utils.base64_url_encode(sp.sharedFolderUid), - 'reshareable': sp.resharable, - 'editable': sp.editable, - 'revision': sp.revision, - 'expiration': sp.expiration, - } for sp in record_info.sharedFolderPermission], - } - record_set.add(record_uid) - except Exception as e: - logger.debug('Error decrypting record "%s": %s', record_uid, e) + _process_record_batch(vault, response, record_keys, record_set) + + +def _process_record_batch(vault: vault_online.VaultOnline, response, record_keys: dict, record_set: set): + """Process a batch of records from API response.""" + for record_info in response.recordDataWithAccessInfo: + record_uid = utils.base64_url_encode(record_info.recordUid) + record_data = record_info.recordData + + try: + if record_data.recordUid and record_data.recordKey: + owner_id = utils.base64_url_encode(record_data.recordUid) + if owner_id in record_keys: + record_keys[record_uid] = crypto.decrypt_aes_v2(record_data.recordKey, record_keys[owner_id]) + + if record_uid not in record_keys: + continue + + record_key = record_keys[record_uid] + version = record_data.version + record = _create_record_dict(record_uid, record_data, record_key, version) + + _handle_record_versions(vault, record, record_data, version, record_set) + _add_share_permissions(record, record_info) + record_set.add(record_uid) + + except Exception as e: + logger.debug('Error decrypting record "%s": %s', record_uid, e) + + +def _create_record_dict(record_uid: str, record_data, record_key: bytes, version: int) -> dict: + """Create record dictionary from API data.""" + return { + 'record_uid': record_uid, + 'revision': record_data.revision, + 'version': version, + 'shared': record_data.shared, + 'data': record_data.encryptedRecordData, + 'record_key_unencrypted': record_key, + 'client_modified_time': record_data.clientModifiedTime, + } + + +def _handle_record_versions(vault: vault_online.VaultOnline, record: dict, record_data, version: int, record_set: set): + """Handle different record versions and their specific features.""" + data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) + record_key = record['record_key_unencrypted'] + + if version <= MAX_V2_VERSION: + record['data_unencrypted'] = crypto.decrypt_aes_v1(data_decoded, record_key) + else: + record['data_unencrypted'] = crypto.decrypt_aes_v2(data_decoded, record_key) + + if record_data.encryptedExtraData and version <= MAX_V2_VERSION: + record['extra'] = record_data.encryptedExtraData + extra_decoded = utils.base64_url_decode(record_data.encryptedExtraData) + record['extra_unencrypted'] = crypto.decrypt_aes_v1(extra_decoded, record_key) + + if version == V3_VERSION: + v3_record = vault.vault_data.load_record(record_uid=record['record_uid']) + if isinstance(v3_record, vault_record.TypedRecord): + for ref in itertools.chain(v3_record.fields, v3_record.custom): + if ref.type.endswith('Ref') and isinstance(ref.value, list): + record_set.update(ref.value) + + elif version == V4_VERSION: + if record_data.fileSize > 0: + record['file_size'] = record_data.fileSize + if record_data.thumbnailSize > 0: + record['thumbnail_size'] = record_data.thumbnailSize + + if record_data.recordUid and record_data.recordKey: + record['owner_uid'] = utils.base64_url_encode(record_data.recordUid) + record['link_key'] = utils.base64_url_encode(record_data.recordKey) + + +def _add_share_permissions(record: dict, record_info): + """Add share permissions to record.""" + record['shares'] = { + 'user_permissions': [{ + 'username': up.username, + 'owner': up.owner, + 'share_admin': up.shareAdmin, + 'shareable': up.sharable, + 'editable': up.editable, + 'awaiting_approval': up.awaitingApproval, + 'expiration': up.expiration, + } for up in record_info.userPermission], + 'shared_folder_permissions': [{ + 'shared_folder_uid': utils.base64_url_encode(sp.sharedFolderUid), + 'reshareable': sp.resharable, + 'editable': sp.editable, + 'revision': sp.revision, + 'expiration': sp.expiration, + } for sp in record_info.sharedFolderPermission], + } def get_record_shares( @@ -292,103 +475,118 @@ def get_record_shares( record_uids: List[str], is_share_admin: bool = False ) -> Optional[List[Dict[str, Any]]]: - record_cache = {x.record_uid: x for x in vault.vault_data.records()} - - def needs_share_info(uid: str) -> bool: - """Check if a record needs share information.""" - if uid in record_cache: - record = record_cache[uid] - return not hasattr(record, 'shares') - return is_share_admin - - def create_record_info(record_uid: str, keeper_record: Optional[Any] = None) -> Dict[str, Any]: - """Create basic record information dictionary.""" - rec = {RECORD_UID_FIELD: record_uid} - - if keeper_record: - if hasattr(keeper_record, TITLE_FIELD): - rec[TITLE_FIELD] = keeper_record.title - if hasattr(keeper_record, 'data_unencrypted'): - rec['data_unencrypted'] = keeper_record.data_unencrypted - - return rec - - def process_user_permissions(info: Any) -> List[Dict[str, Any]]: - """Process user permissions from record info.""" - user_permissions = [] - for up in info.userPermission: - permission = { - 'username': up.username, - 'owner': up.owner, - 'share_admin': up.shareAdmin, - 'shareable': up.sharable, - 'editable': up.editable, - } - if up.awaitingApproval: - permission['awaiting_approval'] = up.awaitingApproval - if up.expiration > 0: - permission['expiration'] = str(up.expiration) - user_permissions.append(permission) - return user_permissions - - def process_shared_folder_permissions(info: Any) -> List[Dict[str, Any]]: - """Process shared folder permissions from record info.""" - shared_folder_permissions = [] - for sp in info.sharedFolderPermission: - permission = { - 'shared_folder_uid': utils.base64_url_encode(sp.sharedFolderUid), - 'reshareable': sp.resharable, - 'editable': sp.editable, - 'revision': sp.revision, - } - if sp.expiration > 0: - permission['expiration'] = sp.expiration - shared_folder_permissions.append(permission) - return shared_folder_permissions - - uids_needing_info = [uid for uid in record_uids if needs_share_info(uid)] + """ + Get share information for records. - if not uids_needing_info: + Args: + vault: VaultOnline instance + record_uids: List of record UIDs + is_share_admin: Whether user is share admin + + Returns: + List of record share information or None + """ + try: + record_cache = {x.record_uid: x for x in vault.vault_data.records()} + + uids_needing_info = [ + uid for uid in record_uids + if _needs_share_info(uid, record_cache, is_share_admin) + ] + + if not uids_needing_info: + return None + + return _fetch_record_shares_batch(vault, uids_needing_info) + + except Exception as e: + logger.error(f"Error fetching record shares: {e}") return None - + + +def _needs_share_info(uid: str, record_cache: dict, is_share_admin: bool) -> bool: + """Check if a record needs share information.""" + if uid in record_cache: + record = record_cache[uid] + return not hasattr(record, 'shares') + return is_share_admin + + +def _fetch_record_shares_batch(vault: vault_online.VaultOnline, uids_needing_info: List[str]) -> List[Dict[str, Any]]: + """Fetch record shares in batches.""" result = [] - try: - chunk_size = CHUNK_SIZE - for i in range(0, len(uids_needing_info), chunk_size): - chunk = uids_needing_info[i:i + chunk_size] + + for i in range(0, len(uids_needing_info), CHUNK_SIZE): + chunk = uids_needing_info[i:i + CHUNK_SIZE] + + request = record_pb2.GetRecordDataWithAccessInfoRequest() + request.clientTime = utils.current_milli_time() + request.recordUid.extend([utils.base64_url_decode(uid) for uid in chunk]) + request.recordDetailsInclude = record_pb2.SHARE_ONLY + + response = vault.keeper_auth.execute_auth_rest( + rest_endpoint=RECORD_DETAILS_URL, + request=request, + response_type=record_pb2.GetRecordDataWithAccessInfoResponse + ) + + if not response or not response.recordDataWithAccessInfo: + logger.error("No response or missing recordDataWithAccessInfo from Keeper API.") + continue - request = record_pb2.GetRecordDataWithAccessInfoRequest() - request.clientTime = utils.current_milli_time() - request.recordUid.extend([utils.base64_url_decode(uid) for uid in chunk]) - request.recordDetailsInclude = record_pb2.SHARE_ONLY + for info in response.recordDataWithAccessInfo: + record_uid = utils.base64_url_encode(info.recordUid) + rec = _create_record_info(record_uid) - response = vault.keeper_auth.execute_auth_rest( - rest_endpoint=RECORD_DETAILS_URL, - request=request, - response_type=record_pb2.GetRecordDataWithAccessInfoResponse - ) + if isinstance(rec, dict): + rec['shares'] = { + 'user_permissions': _process_user_permissions(info), + 'shared_folder_permissions': _process_shared_folder_permissions(info) + } - if not response or not response.recordDataWithAccessInfo: - logger.error("No response or missing recordDataWithAccessInfo from Keeper API.") - continue - - for info in response.recordDataWithAccessInfo: - record_uid = utils.base64_url_encode(info.recordUid) - - rec = create_record_info(record_uid) - - if isinstance(rec, dict): - rec['shares'] = { - 'user_permissions': process_user_permissions(info), - 'shared_folder_permissions': process_shared_folder_permissions(info) - } - - result.append(rec) - - except Exception as e: - logger.error(f"Error fetching record shares: {e}") + result.append(rec) - return result if result else None + return result + + +def _create_record_info(record_uid: str) -> Dict[str, Any]: + """Create basic record information dictionary.""" + return {RECORD_UID_FIELD: record_uid} + + +def _process_user_permissions(info) -> List[Dict[str, Any]]: + """Process user permissions from record info.""" + user_permissions = [] + for up in info.userPermission: + permission = { + 'username': up.username, + 'owner': up.owner, + 'share_admin': up.shareAdmin, + 'shareable': up.sharable, + 'editable': up.editable, + } + if up.awaitingApproval: + permission['awaiting_approval'] = up.awaitingApproval + if up.expiration > 0: + permission['expiration'] = str(up.expiration) + user_permissions.append(permission) + return user_permissions + + +def _process_shared_folder_permissions(info) -> List[Dict[str, Any]]: + """Process shared folder permissions from record info.""" + shared_folder_permissions = [] + for sp in info.sharedFolderPermission: + permission = { + 'shared_folder_uid': utils.base64_url_encode(sp.sharedFolderUid), + 'reshareable': sp.resharable, + 'editable': sp.editable, + 'revision': sp.revision, + } + if sp.expiration > 0: + permission['expiration'] = sp.expiration + shared_folder_permissions.append(permission) + return shared_folder_permissions def resolve_record_share_path(context: KeeperParams, record_uid: str) -> Optional[Dict[str, str]]: @@ -425,7 +623,6 @@ def create_access_path( can_share: bool, team_uid: Optional[str] = None ) -> Dict[str, Any]: - """Create a standardized access path dictionary.""" path = { RECORD_UID_FIELD: record_uid, SHARED_FOLDER_UID_FIELD: shared_folder_uid, @@ -442,7 +639,6 @@ def process_team_permissions( base_can_edit: bool, base_can_share: bool ) -> Generator[Dict[str, Any], None, None]: - """Process team-based permissions for a shared folder.""" if not context.enterprise_data: return @@ -498,7 +694,6 @@ def get_shared_records(context: KeeperParams, record_uids, cache_only=False): """ def _fetch_team_members_from_api(team_uids: Set[str]) -> Dict[str, Set[str]]: - """Fetch team members from the API for the given team UIDs.""" members = {} if not context.vault.keeper_auth.auth_context.enterprise_ec_public_key: @@ -525,7 +720,6 @@ def _fetch_team_members_from_api(team_uids: Set[str]) -> Dict[str, Set[str]]: return members def _get_cached_team_members(team_uids: Set[str], username_lookup: Dict[str, str]) -> Dict[str, Set[str]]: - """Get team members from cached enterprise data.""" members = {} if not context.enterprise_data: @@ -549,7 +743,6 @@ def _get_cached_team_members(team_uids: Set[str], username_lookup: Dict[str, str return members def _fetch_shared_folder_admins() -> Dict[str, List[str]]: - """Fetch share administrators for all shared folders.""" sf_uids = list(context.vault.vault_data._shared_folders.keys()) return { sf_uid: get_share_admins_for_shared_folder(context.vault, sf_uid) or [] @@ -557,7 +750,6 @@ def _fetch_shared_folder_admins() -> Dict[str, List[str]]: } def _get_restricted_role_members(username_lookup: Dict[str, str]) -> Set[str]: - """Get usernames with restricted sharing permissions.""" if not context.enterprise_data: return set() @@ -589,7 +781,8 @@ def _get_restricted_role_members(username_lookup: Dict[str, str]) -> Set[str]: return restricted_members try: - shares = get_record_shares(context.vault, record_uids) + vault = context.vault + shares = get_record_shares(vault, record_uids) sf_teams = [share.get('teams', []) for share in shares] if shares else [] team_uids = { @@ -611,15 +804,17 @@ def _get_restricted_role_members(username_lookup: Dict[str, str]) -> Set[str]: else: team_members = _fetch_team_members_from_api(team_uids) - records = [context.vault.vault_data.get_record(uid) for uid in record_uids] + records = [vault.vault_data.get_record(uid) for uid in record_uids] valid_records = [record for record in records if record is not None] + from .share_record import SharedRecord + shared_records = [ - SharedRecord(record, sf_share_admins, team_members, restricted_role_members) + SharedRecord(context, record, sf_share_admins, team_members, restricted_role_members) for record in valid_records ] - return {shared_record.record_uid: shared_record for shared_record in shared_records} + return {shared_record.uid: shared_record for shared_record in shared_records} except Exception as e: logger.error(f"Error in get_shared_records: {e}") @@ -644,7 +839,6 @@ def get_share_admins_for_shared_folder(vault: vault_online.VaultOnline, shared_f def get_folder_uids(context: KeeperParams, name: str) -> set[str]: - """Get folder UIDs by name or path.""" folder_uids = set() if not context.vault or not context.vault.vault_data: @@ -688,40 +882,3 @@ def on_folder(f): vault_utils.traverse_folder_tree(vault.vault_data, folder, on_folder) return records_by_folder - - -class SharedRecord: - - def __init__(self, record, sf_share_admins, team_members, restricted_role_members): - self.record = record - self.sf_share_admins = sf_share_admins or {} - self.team_members = team_members or {} - self.restricted_role_members = restricted_role_members or set() - - @property - def record_uid(self) -> str: - return self.record.record_uid - - @property - def title(self) -> str: - return getattr(self.record, 'title', '') - - def get_all_share_admins(self) -> List[str]: - admin_usernames = [] - for sf_uid, admins_list in self.sf_share_admins.items(): - if admins_list: - admin_usernames.extend(admins_list) - return list(set(admin_usernames)) # Remove duplicates - - def get_share_admins_for_folder(self, sf_uid: str) -> List[str]: - return self.sf_share_admins.get(sf_uid, []) - - def get_all_team_members(self) -> Set[str]: - all_members = set() - for team_uid, members in self.team_members.items(): - if members: - all_members.update(members) - return all_members - - def is_restricted_user(self, username: str) -> bool: - return username in self.restricted_role_members \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index ef47ae95..43a3085f 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -45,6 +45,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('search', record_edit.RecordSearchCommand(), base.CommandScope.Vault, 's') commands.register_command('record-history', record_handling_commands.RecordHistoryCommand(), base.CommandScope.Vault, 'rh') commands.register_command('clipboard-copy', record_handling_commands.ClipboardCommand(), base.CommandScope.Vault, 'cc') + commands.register_command('find-duplicate', record_handling_commands.FindDuplicateCommand(), base.CommandScope.Vault) commands.register_command('find-password', record_handling_commands.ClipboardCommand(), base.CommandScope.Vault) commands.register_command('find-ownerless', register.FindOwnerlessCommand(), base.CommandScope.Vault) commands.register_command('record-add', record_edit.RecordAddCommand(), base.CommandScope.Vault, 'ra') diff --git a/keepersdk-package/src/keepersdk/vault/record_management.py b/keepersdk-package/src/keepersdk/vault/record_management.py index a362ac76..6a698180 100644 --- a/keepersdk-package/src/keepersdk/vault/record_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_management.py @@ -300,13 +300,16 @@ def delete_vault_objects(vault: vault_online.VaultOnline, objects.append(obj) else: record = vault.vault_data.get_record(to_delete) - # TODO resolve folder + folders = vault_utils.get_folders_for_record(vault.vault_data, record.record_uid) + if folders: + folder = folders[0] if record: obj = { 'object_uid': record.record_uid, 'object_type': 'record', 'delete_resolution': 'unlink', 'from_type': 'user_folder', + 'from_uid': folder.folder_uid, } objects.append(obj) elif isinstance(to_delete, vault_types.RecordPath): From 48662cc6b06ec4ca26310305f9b00b0652158932 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Mon, 27 Oct 2025 22:02:45 +0530 Subject: [PATCH 39/44] Transfer user command added --- .../keepercli/commands/transfer_account.py | 473 +++++++++++++++ .../src/keepercli/register_commands.py | 3 +- .../keepersdk/enterprise/account_transfer.py | 564 ++++++++++++++++++ 3 files changed, 1039 insertions(+), 1 deletion(-) create mode 100644 keepercli-package/src/keepercli/commands/transfer_account.py create mode 100644 keepersdk-package/src/keepersdk/enterprise/account_transfer.py diff --git a/keepercli-package/src/keepercli/commands/transfer_account.py b/keepercli-package/src/keepercli/commands/transfer_account.py new file mode 100644 index 00000000..2f57bc9a --- /dev/null +++ b/keepercli-package/src/keepercli/commands/transfer_account.py @@ -0,0 +1,473 @@ +import argparse +import os +import logging +from typing import Optional, Dict, Set +from enum import Enum + +from keepersdk.enterprise.account_transfer import AccountTransferManager +from keepersdk.authentication import keeper_auth +from keepersdk.enterprise import enterprise_types + +from . import base +from ..params import KeeperParams + +logger = logging.getLogger(__name__) + + +# Enums for user status and lock states +class UserStatus(Enum): + ACTIVE = 'active' + INVITED = 'invited' + INACTIVE = 'inactive' + + +class UserLockState(Enum): + UNLOCKED = 0 + LOCKED = 1 + DISABLED = 2 + + +class UserLockText(Enum): + LOCKED = 'Locked' + DISABLED = 'Disabled' + + +# ANSI color codes - reusable across the application +class Colors(Enum): + RED = '\033[91m' + RESET = '\033[0m' + + +# Sample mapping file content for documentation +SAMPLE_MAPPING_FILE_CONTENT = """ +# Lines starting with #, ;, or - are comments +john.doe@company.com -> admin@company.com +jane.smith@company.com <- admin@company.com +old.user@company.com = new.admin@company.com +user1@company.com user2@company.com +""" + + +class EnterpriseTransferAccountCommand(base.ArgparseCommand): + """Perform a vault transfer of a user account + + This command transfers all vault data (records, shared folders, teams, + user folders) from one or more source users to a target user. + """ + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='transfer-user', + description='Transfer user account from one user to another' + ) + EnterpriseTransferAccountCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '-f', '--force', + dest='force', + action='store_true', + help='do not prompt for confirmation' + ) + parser.add_argument( + '--target-user', + dest='target_user', + action='store', + help='email to transfer user(s) to' + ) + parser.add_argument( + 'email', + type=str, + nargs='+', + metavar="user@company.com OR @filename", + help='User account email/ID (list of strings["user1@company.com", "user2@company.com"]) or File containing account mappings. ' + 'Use @filename to indicate using mapping file. ' + f'File format examples:{SAMPLE_MAPPING_FILE_CONTENT}' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute the transfer account command + + Args: + context: Keeper parameters with auth and enterprise data + **kwargs: Command arguments (force, target_user, email) + """ + if not context.vault: + raise ValueError('Vault not available. Please ensure you are logged in.') + + enterprise_loader = context.enterprise_loader + if not enterprise_loader: + raise ValueError('Enterprise data not available. Please ensure you are logged in as an enterprise admin.') + + # Ensure enterprise data is loaded + logger.info('Loading enterprise data...') + enterprise_loader.load() + + user_lookup = self._build_user_lookup(enterprise_loader.enterprise_data) + logger.debug(f'Loaded {len(user_lookup)} entries in user lookup table') + + transfer_map = self._parse_transfer_arguments(kwargs, user_lookup) + + if not transfer_map: + logger.warning('No user accounts to transfer') + return + + transfer_map = self._validate_transfer_map(transfer_map) + + if not transfer_map: + logger.warning('No valid user accounts to transfer after validation') + return + + if not self._confirm_transfer(kwargs, transfer_map): + logger.info('Transfer cancelled by user') + return + + auth_context = context.auth + self._lock_source_users(auth_context, transfer_map, user_lookup) + + target_keys = self._load_target_keys(auth_context, transfer_map) + + self._execute_transfers(context, transfer_map, target_keys, user_lookup) + + logger.info('Reloading enterprise data...') + enterprise_loader.load(reset=True) + + def _build_user_lookup(self, enterprise_data: enterprise_types.IEnterpriseData) -> Dict: + + user_lookup = {} + + for user in enterprise_data.users.get_all_entities(): + user_dict = { + 'enterprise_user_id': user.enterprise_user_id, + 'username': user.username, + 'status': user.status, + 'lock': user.lock + } + + # Store by user ID + user_lookup[str(user.enterprise_user_id)] = user_dict + + # Store by username (both original case and lowercase) + if user.username: + user_lookup[user.username] = user_dict + user_lookup[user.username.lower()] = user_dict + else: + logger.debug(f'Username missing from user id={user.enterprise_user_id}') + + return user_lookup + + def _parse_transfer_arguments(self, + kwargs, + user_lookup: Dict) -> Dict[str, Set[str]]: + transfer_map = {} + target_user = kwargs.get('target_user') + + if target_user: + target_user = self._verify_user(target_user, user_lookup) + + for email in kwargs.get('email', []): + if email.startswith('@'): + # File input + self._parse_transfer_file(email[1:], transfer_map, user_lookup) + else: + # Single user + email = self._verify_user(email, user_lookup) + if email and target_user: + if target_user not in transfer_map: + transfer_map[target_user] = set() + transfer_map[target_user].add(email) + + return transfer_map + + def _parse_transfer_file(self, + filename: str, + transfer_map: Dict, + user_lookup: Dict): + """Parse transfer mapping file + + Sample mapping file content: + # Lines starting with #, ;, or - are comments + john.doe@company.com -> admin@company.com + jane.smith@company.com <- admin@company.com + old.user@company.com = new.admin@company.com + user1@company.com user2@company.com + + Args: + filename: Path to mapping file + transfer_map: Transfer map to populate + user_lookup: User lookup dictionary + """ + if not os.path.exists(filename): + logger.warning(f'File "{filename}" does not exist. Skipping...') + return + + with open(filename, 'r', encoding='utf-8') as f: + lines = f.readlines() + + for line_num, line in enumerate(lines, 1): + line = line.strip() + + # Skip empty lines and comments + if not line or line[0] in {'#', ';', '-'}: + continue + + # Parse mapping: from -> to + p = line.partition('->') + if p[1] != '->': + p = line.partition('<-') + if p[1] != '<-': + p = line.partition('=') + if p[1] != '=': + p = line.partition(' ') + + if p[2]: + user1 = self._verify_user(p[0], user_lookup) + if user1: + user2 = self._verify_user(p[2], user_lookup) + if user2: + # Determine direction + if p[1] == '<-': + from_user, to_user = user2, user1 + else: + from_user, to_user = user1, user2 + + if to_user not in transfer_map: + transfer_map[to_user] = set() + transfer_map[to_user].add(from_user) + else: + logger.warning(f'File "{filename}" line {line_num}: invalid mapping "{line}". Skipping...') + + def _verify_user(self, + username: str, + user_lookup: Dict) -> Optional[str]: + """Verify user exists and is active + """ + username_clean = username.strip() + username_lower = username_clean.lower() + + enterprise_user = None + if username_clean in user_lookup: + enterprise_user = user_lookup[username_clean] + elif username_lower in user_lookup: + enterprise_user = user_lookup[username_lower] + + if not enterprise_user: + logger.warning(f'"{username}" is not a known user account. Skipping...') + logger.debug(f'Available users in lookup: {list(user_lookup.keys())}') + return None + + # Check if user is effectively active (active status + not locked) + status = enterprise_user['status'] + lock = enterprise_user.get('lock', UserLockState.UNLOCKED.value) + + if status == UserStatus.INVITED.value: + logger.warning(f'"{username}" is a pending account. Skipping...') + return None + elif status != UserStatus.ACTIVE.value: + logger.warning(f'"{username}" is not an active account (status: {status}). Skipping...') + return None + elif lock > UserLockState.UNLOCKED.value: + lock_text = UserLockText.LOCKED.value if lock == UserLockState.LOCKED.value else UserLockText.DISABLED.value + logger.warning(f'"{username}" account is {lock_text}. Skipping...') + return None + + return enterprise_user['username'] + + def _validate_transfer_map(self, transfer_map: Dict) -> Dict: + + # Check for users that appear as both source and target (circular) + targets = set(transfer_map.keys()) + sources = set() + for target in transfer_map: + sources.update(transfer_map[target]) + + circular = targets.intersection(sources) + if circular: + for email in circular: + logger.warning( + f'User "{email}" appears as both source and target for account transfer. ' + 'This is not allowed. Removing from transfer map...' + ) + if email in transfer_map: + del transfer_map[email] + + for target in list(transfer_map.keys()): + transfer_map[target].difference_update(circular) + if not transfer_map[target]: + del transfer_map[target] + + sources.clear() + duplicates = set() + for target in transfer_map: + dups = transfer_map[target].intersection(sources) + if dups: + duplicates.update(dups) + sources.update(transfer_map[target]) + + if duplicates: + for email in duplicates: + logger.warning( + f'User "{email}" cannot be moved to multiple targets. Removing...' + ) + + for target in list(transfer_map.keys()): + transfer_map[target].difference_update(duplicates) + if not transfer_map[target]: + del transfer_map[target] + + return transfer_map + + def _confirm_transfer(self, kwargs, transfer_map: Dict) -> bool: + if kwargs.get('force'): + return True + + source_count = sum(len(sources) for sources in transfer_map.values()) + + logger.info(f'\n{Colors.RED.value}ALERT!{Colors.RESET.value}') + logger.info('This action cannot be undone.') + logger.info(f'Do you want to proceed with transferring {source_count} account(s)? [y/n]: ') + answer = input().strip().lower() + return answer == 'y' + + def _lock_source_users(self, + auth: keeper_auth.KeeperAuth, + transfer_map: Dict, + user_lookup: Dict): + """Lock source users before transfer + """ + sources = set() + for target in transfer_map: + sources.update(transfer_map[target]) + + lock_requests = [ + { + 'command': 'enterprise_user_lock', + 'enterprise_user_id': user_lookup[email]['enterprise_user_id'], + 'lock': 'locked' + } + for email in sources + if user_lookup[email].get('lock', UserLockState.UNLOCKED.value) != UserLockState.LOCKED.value + ] + + if lock_requests: + logger.info('Locking active users.') + auth.execute_batch(lock_requests) + + def _load_target_keys(self, + auth: keeper_auth.KeeperAuth, + transfer_map: Dict) -> Dict: + """Load public keys for all target users + """ + target_users = list(transfer_map.keys()) + + logger.info(f'Loading public keys for {len(target_users)} target user(s)...') + + auth.load_user_public_keys(target_users, send_invites=False) + + # Collect loaded keys + target_keys = {} + for target_user in target_users: + user_keys = auth.get_user_keys(target_user) + if user_keys: + target_keys[target_user] = user_keys + logger.debug(f'Loaded keys for {target_user}') + else: + logger.warning(f'Failed to get public key for "{target_user}". Transfer will be skipped.') + del transfer_map[target_user] + + return target_keys + + def _execute_transfers(self, + context: KeeperParams, + transfer_map: Dict, + target_keys: Dict, + user_lookup: Dict): + + transfer_manager = AccountTransferManager( + context.enterprise_loader, + context.auth + ) + + total_transfers = sum(len(sources) for sources in transfer_map.values()) + completed = 0 + failed = 0 + + logger.debug(f'Transfer map contents: {dict(transfer_map)}') + + for target_user, source_users in transfer_map.items(): + if target_user not in target_keys: + logger.warning(f'Skipping transfers to {target_user} (no public key)') + failed += len(source_users) + continue + + for source_user in source_users: + try: + logger.info(f'[{completed+1}/{total_transfers}] Transferring {source_user} to {target_user}...') + + result = transfer_manager.transfer_account( + source_user, + target_user, + target_keys[target_user] + ) + + if result.success: + completed += 1 + logger.info(f'{source_user}: Account transferred successfully') + + if result.records_transferred > 0: + logger.info(f'Records: {result.records_transferred}') + if result.shared_folders_transferred > 0: + logger.info(f'Shared Folders: {result.shared_folders_transferred}') + if result.teams_transferred > 0: + logger.info(f'Teams: {result.teams_transferred}') + if result.user_folders_transferred > 0: + logger.info(f' User Folders: {result.user_folders_transferred}') + + # Show warnings for corrupted items + if result.corrupted_records > 0: + logger.warning(f'Corrupted records skipped: {result.corrupted_records}') + if result.corrupted_shared_folders > 0: + logger.warning(f'Corrupted shared folders skipped: {result.corrupted_shared_folders}') + if result.corrupted_teams > 0: + logger.warning(f'Corrupted teams skipped: {result.corrupted_teams}') + if result.corrupted_user_folders > 0: + logger.warning(f'Corrupted user folders skipped: {result.corrupted_user_folders}') + + except Exception as e: + failed += 1 + logger.error(f'Failed to transfer {source_user}: {e}') + # Unlock source user if transfer fails + self._unlock_source_users(context.auth, source_user, user_lookup) + + if failed > 0: + logger.error(f'Failed transfers: {failed}') + + def _unlock_source_users(self, + auth: keeper_auth.KeeperAuth, + source_user: str, + user_lookup: Dict): + """Unlock source user if transfer fails + """ + if source_user not in user_lookup: + logger.warning(f'Cannot unlock {source_user}: user not found in lookup') + return + + user_data = user_lookup[source_user] + enterprise_user_id = user_data.get('enterprise_user_id') + + if not enterprise_user_id: + logger.warning(f'Cannot unlock {source_user}: enterprise_user_id not found') + return + + unlock_requests = [ + { + 'command': 'enterprise_user_lock', + 'enterprise_user_id': enterprise_user_id, + 'lock': 'unlocked' + } + ] + + logger.info(f'Unlocking {source_user}...') + auth.execute_batch(unlock_requests) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 43a3085f..d4b52c70 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -79,7 +79,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Enterprise): from .commands import (enterprise_info, enterprise_node, enterprise_role, enterprise_team, enterprise_user, enterprise_create_user, - importer_commands, audit_report, audit_alert, audit_log) + importer_commands, audit_report, audit_alert, audit_log, transfer_account) commands.register_command('create-user', enterprise_create_user.CreateEnterpriseUserCommand(), base.CommandScope.Enterprise, 'ecu') commands.register_command('enterprise-down', enterprise_info.EnterpriseDownCommand(), base.CommandScope.Enterprise, 'ed') @@ -88,6 +88,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('enterprise-role', enterprise_role.EnterpriseRoleCommand(), base.CommandScope.Enterprise, 'er') commands.register_command('enterprise-team', enterprise_team.EnterpriseTeamCommand(), base.CommandScope.Enterprise, 'et') commands.register_command('enterprise-user', enterprise_user.EnterpriseUserCommand(), base.CommandScope.Enterprise, 'eu') + commands.register_command('transfer-user', transfer_account.EnterpriseTransferAccountCommand(), base.CommandScope.Enterprise) commands.register_command('audit-report', audit_report.EnterpriseAuditReport(), base.CommandScope.Enterprise) commands.register_command('audit-alert', audit_alert.AuditAlerts(), base.CommandScope.Enterprise) commands.register_command('audit-log', audit_log.AuditLogCommand(), base.CommandScope.Enterprise, 'al') diff --git a/keepersdk-package/src/keepersdk/enterprise/account_transfer.py b/keepersdk-package/src/keepersdk/enterprise/account_transfer.py new file mode 100644 index 00000000..39ffd807 --- /dev/null +++ b/keepersdk-package/src/keepersdk/enterprise/account_transfer.py @@ -0,0 +1,564 @@ +import json +import logging +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional, Dict, List, Tuple, Any + +from keepersdk import crypto, utils +from keepersdk.authentication import keeper_auth +from keepersdk.enterprise import enterprise_types + +logger = logging.getLogger(__name__) + + +# Constants for encryption types +class EncryptionType(Enum): + ENCRYPTED_BY_DATA_KEY = 'encrypted_by_data_key' + ENCRYPTED_BY_PUBLIC_KEY = 'encrypted_by_public_key' + ENCRYPTED_BY_DATA_KEY_GCM = 'encrypted_by_data_key_gcm' + ENCRYPTED_BY_PUBLIC_KEY_ECC = 'encrypted_by_public_key_ecc' + + +# Transfer data keys +class TransferDataKeys(Enum): + RECORD_KEYS = 'record_keys' + CORRUPTED_RECORD_KEYS = 'corrupted_record_keys' + SHARED_FOLDER_KEYS = 'shared_folder_keys' + CORRUPTED_SHARED_FOLDER_KEYS = 'corrupted_shared_folder_keys' + TEAM_KEYS = 'team_keys' + CORRUPTED_TEAM_KEYS = 'corrupted_team_keys' + USER_FOLDER_KEYS = 'user_folder_keys' + CORRUPTED_USER_FOLDER_KEYS = 'corrupted_user_folder_keys' + USER_FOLDER_TRANSFER = 'user_folder_transfer' + +class TransferKeyType(Enum): + RAW_DATA_KEY = 0 + ENCRYPTED_BY_DATA_KEY = 1 + ENCRYPTED_BY_RSA = 2 + ENCRYPTED_BY_DATA_KEY_GCM = 3 + ENCRYPTED_BY_ECC = 4 + + +@dataclass +class PreTransferResponse: + transfer_key2: Optional[bytes] = None + transfer_key2_type_id: Optional[int] = None + transfer_key: Optional[bytes] = None + transfer_key_type_id: Optional[int] = None + role_key: Optional[bytes] = None + role_key_id: Optional[int] = None + role_private_key: Optional[bytes] = None + user_private_key: Optional[bytes] = None + user_ecc_private_key: Optional[bytes] = None + record_keys: List[Dict[str, Any]] = field(default_factory=list) + shared_folder_keys: List[Dict[str, Any]] = field(default_factory=list) + team_keys: List[Dict[str, Any]] = field(default_factory=list) + user_folder_keys: List[Dict[str, Any]] = field(default_factory=list) + + +@dataclass +class TransferResult: + success: bool + username: str + records_transferred: int = 0 + shared_folders_transferred: int = 0 + teams_transferred: int = 0 + user_folders_transferred: int = 0 + corrupted_records: int = 0 + corrupted_shared_folders: int = 0 + corrupted_teams: int = 0 + corrupted_user_folders: int = 0 + error_message: Optional[str] = None + + +class AccountTransferError(Exception): + """Exception raised during account transfer operations""" + + +class AccountTransferManager: + + def __init__(self, + loader: enterprise_types.IEnterpriseLoader, + auth: keeper_auth.KeeperAuth): + self.loader = loader + self.auth = auth + + def transfer_account(self, + from_username: str, + to_username: str, + target_public_keys: keeper_auth.UserKeys) -> TransferResult: + + try: + logger.info(f'Starting account transfer from {from_username} to {to_username}') + + pre_transfer = self._execute_pre_transfer(from_username) + + user_data_key = self._decrypt_user_data_key(pre_transfer) + + user_rsa_private_key = self._decrypt_user_rsa_key(pre_transfer, user_data_key) + user_ecc_private_key = self._decrypt_user_ecc_key(pre_transfer, user_data_key) + + transfer_data = self._prepare_transfer_data( + pre_transfer, + user_data_key, + user_rsa_private_key, + user_ecc_private_key, + target_public_keys, + from_username + ) + + result = self._execute_transfer(from_username, to_username, transfer_data) + + logger.info(f'Account transfer completed successfully for {from_username}') + return result + + except Exception as e: + raise AccountTransferError(f'Failed to transfer {from_username}: {e}') from e + + def _execute_pre_transfer(self, username: str) -> PreTransferResponse: + rq = { + 'command': 'pre_account_transfer', + 'target_username': username + } + + rs = self.auth.execute_auth_command(rq) + return self._parse_pre_transfer_response(rs) + + def _parse_pre_transfer_response(self, rs: Dict) -> PreTransferResponse: + """Parse pre_account_transfer response into dataclass""" + # Keys that need base64 decoding + base64_keys = ['transfer_key2', 'transfer_key', 'role_key', 'role_private_key', + 'user_private_key', 'user_ecc_private_key'] + + decoded_data = { + key: utils.base64_url_decode(rs[key]) if key in rs else None + for key in base64_keys + } + + decoded_data.update({ + 'transfer_key2_type_id': rs.get('transfer_key2_type_id'), + 'transfer_key_type_id': rs.get('transfer_key_type_id'), + 'role_key_id': rs.get('role_key_id'), + 'record_keys': rs.get('record_keys', []), + 'shared_folder_keys': rs.get('shared_folder_keys', []), + 'team_keys': rs.get('team_keys', []), + 'user_folder_keys': rs.get('user_folder_keys', []) + }) + + return PreTransferResponse(**decoded_data) + + def _decrypt_user_data_key(self, pre_transfer: PreTransferResponse) -> bytes: + + if pre_transfer.transfer_key2: + return self._decrypt_transfer_key2( + pre_transfer.transfer_key2, + pre_transfer.transfer_key2_type_id + ) + + + if pre_transfer.transfer_key: + return self._decrypt_legacy_transfer_key(pre_transfer) + + raise AccountTransferError('No valid transfer key found in response') + + def _decrypt_transfer_key2(self, encrypted_key: bytes, key_type: int) -> bytes: + + enterprise_data = self.loader.enterprise_data + tree_key = enterprise_data.enterprise_info.tree_key + + if key_type == TransferKeyType.ENCRYPTED_BY_DATA_KEY.value: + return crypto.decrypt_aes_v1(encrypted_key, tree_key) + + elif key_type == TransferKeyType.ENCRYPTED_BY_RSA.value: + private_key = enterprise_data.enterprise_info.rsa_private_key + if not private_key: + raise AccountTransferError('Enterprise RSA private key not available') + return crypto.decrypt_rsa(encrypted_key, private_key) + + elif key_type == TransferKeyType.ENCRYPTED_BY_DATA_KEY_GCM.value: + return crypto.decrypt_aes_v2(encrypted_key, tree_key) + + elif key_type == TransferKeyType.ENCRYPTED_BY_ECC.value: + private_key = enterprise_data.enterprise_info.ec_private_key + if not private_key: + raise AccountTransferError('Enterprise ECC private key not available') + return crypto.decrypt_ec(encrypted_key, private_key) + + raise AccountTransferError(f'Unsupported transfer key type: {key_type}') + + def _decrypt_legacy_transfer_key(self, pre_transfer: PreTransferResponse) -> bytes: + enterprise_data = self.loader.enterprise_data + tree_key = enterprise_data.enterprise_info.tree_key + + role_key = None + if pre_transfer.role_key: + rsa_private_key = self.auth.auth_context.rsa_private_key + if not rsa_private_key: + raise AccountTransferError('RSA private key not available for role key decryption') + role_key = crypto.decrypt_rsa( + pre_transfer.role_key, + rsa_private_key) + elif pre_transfer.role_key_id: + role_key_id = pre_transfer.role_key_id + role_keys2 = enterprise_data.role_keys.get_all_entities() + key_entry = next((x for x in role_keys2 if x.role_id == role_key_id), None) + if key_entry: + role_key = utils.base64_url_decode(key_entry.encrypted_key) + role_key = crypto.decrypt_aes_v2(role_key, tree_key) + + if not role_key: + raise AccountTransferError('Cannot decrypt role key') + + if not pre_transfer.role_private_key: + raise AccountTransferError('Role private key not found in response') + + role_private_key_bytes = crypto.decrypt_aes_v1( + pre_transfer.role_private_key, role_key) + role_private_key = crypto.load_rsa_private_key(role_private_key_bytes) + + return crypto.decrypt_rsa(pre_transfer.transfer_key, role_private_key) + + def _decrypt_user_rsa_key(self, + pre_transfer: PreTransferResponse, + user_data_key: bytes) -> Optional[Any]: + user_private_key = pre_transfer.user_private_key + if not user_private_key: + return None + + decrypted = crypto.decrypt_aes_v1( + user_private_key, user_data_key) + return crypto.load_rsa_private_key(decrypted) + + def _decrypt_user_ecc_key(self, + pre_transfer: PreTransferResponse, + user_data_key: bytes) -> Optional[Any]: + user_ecc_private_key = pre_transfer.user_ecc_private_key + if not user_ecc_private_key: + return None + + decrypted = crypto.decrypt_aes_v2( + user_ecc_private_key, user_data_key) + return crypto.load_ec_private_key(decrypted) + + def _prepare_transfer_data(self, + pre_transfer: PreTransferResponse, + user_data_key: bytes, + user_rsa_private_key: Optional[Any], + user_ecc_private_key: Optional[Any], + target_keys: keeper_auth.UserKeys, + from_username: str) -> Dict: + + transfer_data = {} + + key_processors = [ + ('record_keys', self._reencrypt_record_keys), + ('shared_folder_keys', self._reencrypt_shared_folder_keys), + ('team_keys', self._reencrypt_team_keys) + ] + + for key_attr, processor_func in key_processors: + keys = getattr(pre_transfer, key_attr, None) + if keys: + reencrypted, corrupted = processor_func( + keys, user_data_key, user_rsa_private_key, user_ecc_private_key, target_keys + ) + transfer_data[key_attr] = reencrypted + transfer_data[f'corrupted_{key_attr}'] = corrupted + + uf_keys, corrupted, transfer_folder = self._reencrypt_user_folder_keys( + pre_transfer.user_folder_keys, + user_data_key, + user_rsa_private_key, + user_ecc_private_key, + target_keys, + from_username + ) + if uf_keys: + transfer_data[TransferDataKeys.USER_FOLDER_KEYS.value] = uf_keys + if corrupted: + transfer_data[TransferDataKeys.CORRUPTED_USER_FOLDER_KEYS.value] = corrupted + transfer_data[TransferDataKeys.USER_FOLDER_TRANSFER.value] = transfer_folder + + return transfer_data + + def _reencrypt_record_keys(self, + record_keys: List[Dict], + user_data_key: bytes, + user_rsa_key: Optional[Any], + user_ecc_key: Optional[Any], + target_keys: keeper_auth.UserKeys) -> Tuple[List[Dict], List[Dict]]: + + reencrypted = [] + corrupted = [] + + for rk in record_keys: + try: + record_key = self._decrypt_key_by_type( + utils.base64_url_decode(rk['record_key']), + rk.get('record_key_type', 1), + user_data_key, + user_rsa_key, + user_ecc_key + ) + + encrypted_key, key_type = self._encrypt_for_target( + record_key, target_keys) + + reencrypted.append({ + 'record_uid': rk['record_uid'], + 'record_key': utils.base64_url_encode(encrypted_key), + 'record_key_type': key_type + }) + except Exception as e: + logger.debug(f"Corrupted record key {rk.get('record_uid', 'unknown')}: {e}") + corrupted.append(rk) + + return reencrypted, corrupted + + def _reencrypt_shared_folder_keys(self, + sf_keys: List[Dict], + user_data_key: bytes, + user_rsa_key: Optional[Any], + user_ecc_key: Optional[Any], + target_keys: keeper_auth.UserKeys) -> Tuple[List[Dict], List[Dict]]: + + reencrypted = [] + corrupted = [] + forbid_rsa = self.auth.auth_context.forbid_rsa + + for sfk in sf_keys: + try: + sf_key = self._decrypt_key_by_type( + utils.base64_url_decode(sfk['shared_folder_key']), + sfk.get('shared_folder_key_type', 1), + user_data_key, + user_rsa_key, + user_ecc_key + ) + + if forbid_rsa: + encrypted_key, key_type = self._encrypt_for_target(sf_key, target_keys) + else: + if target_keys.aes: + encrypted_key = crypto.encrypt_aes_v1(sf_key, target_keys.aes) + key_type = EncryptionType.ENCRYPTED_BY_DATA_KEY.value + elif target_keys.rsa: + rsa_key = crypto.load_rsa_public_key(target_keys.rsa) + encrypted_key = crypto.encrypt_rsa(sf_key, rsa_key) + key_type = EncryptionType.ENCRYPTED_BY_PUBLIC_KEY.value + else: + raise Exception('No valid target key for shared folder') + + reencrypted.append({ + 'shared_folder_uid': sfk['shared_folder_uid'], + 'shared_folder_key': utils.base64_url_encode(encrypted_key), + 'shared_folder_key_type': key_type + }) + except Exception as e: + logger.debug(f"Corrupted SF key {sfk.get('shared_folder_uid', 'unknown')}: {e}") + corrupted.append(sfk) + + return reencrypted, corrupted + + def _reencrypt_team_keys(self, + team_keys: List[Dict], + user_data_key: bytes, + user_rsa_key: Optional[Any], + user_ecc_key: Optional[Any], + target_keys: keeper_auth.UserKeys) -> Tuple[List[Dict], List[Dict]]: + + reencrypted = [] + corrupted = [] + forbid_rsa = self.auth.auth_context.forbid_rsa + + for tk in team_keys: + try: + team_key = self._decrypt_key_by_type( + utils.base64_url_decode(tk['team_key']), + tk.get('team_key_type', 1), + user_data_key, + user_rsa_key, + user_ecc_key + ) + + if forbid_rsa: + encrypted_key, key_type = self._encrypt_for_target(team_key, target_keys) + else: + if target_keys.aes: + encrypted_key = crypto.encrypt_aes_v1(team_key, target_keys.aes) + key_type = EncryptionType.ENCRYPTED_BY_DATA_KEY.value + elif target_keys.rsa: + rsa_key = crypto.load_rsa_public_key(target_keys.rsa) + encrypted_key = crypto.encrypt_rsa(team_key, rsa_key) + key_type = EncryptionType.ENCRYPTED_BY_PUBLIC_KEY.value + else: + raise Exception('No valid target key for team') + + reencrypted.append({ + 'team_uid': tk['team_uid'], + 'team_key': utils.base64_url_encode(encrypted_key), + 'team_key_type': key_type + }) + except Exception as e: + logger.debug(f"Corrupted team key {tk.get('team_uid', 'unknown')}: {e}") + corrupted.append(tk) + + return reencrypted, corrupted + + def _reencrypt_user_folder_keys(self, + uf_keys: List[Dict], + user_data_key: bytes, + user_rsa_key: Optional[Any], + user_ecc_key: Optional[Any], + target_keys: keeper_auth.UserKeys, + from_username: str) -> Tuple[List[Dict], List[Dict], Dict]: + + reencrypted = [] + corrupted = [] + forbid_rsa = self.auth.auth_context.forbid_rsa + + folder_key = utils.generate_aes_key() + folder_name = f'Transfer from {from_username}' + folder_data = json.dumps({'name': folder_name}).encode('utf-8') + folder_data = crypto.encrypt_aes_v1(folder_data, folder_key) + + if forbid_rsa: + if target_keys.aes: + encrypted_folder_key = crypto.encrypt_aes_v2(folder_key, target_keys.aes) + folder_key_type = EncryptionType.ENCRYPTED_BY_DATA_KEY_GCM.value + elif target_keys.ec: + ec_key = crypto.load_ec_public_key(target_keys.ec) + encrypted_folder_key = crypto.encrypt_ec(folder_key, ec_key) + folder_key_type = EncryptionType.ENCRYPTED_BY_PUBLIC_KEY_ECC.value + else: + raise Exception('No valid target key for transfer folder') + else: + if target_keys.aes: + encrypted_folder_key = crypto.encrypt_aes_v1(folder_key, target_keys.aes) + folder_key_type = EncryptionType.ENCRYPTED_BY_DATA_KEY.value + elif target_keys.rsa: + rsa_key = crypto.load_rsa_public_key(target_keys.rsa) + encrypted_folder_key = crypto.encrypt_rsa(folder_key, rsa_key) + folder_key_type = EncryptionType.ENCRYPTED_BY_PUBLIC_KEY.value + else: + raise Exception('No valid target key for transfer folder') + + transfer_folder = { + 'transfer_folder_uid': utils.generate_uid(), + 'transfer_folder_key': utils.base64_url_encode(encrypted_folder_key), + 'transfer_folder_key_type': folder_key_type, + 'transfer_folder_data': utils.base64_url_encode(folder_data) + } + + for ufk in uf_keys: + try: + uf_key = self._decrypt_key_by_type( + utils.base64_url_decode(ufk['user_folder_key']), + ufk.get('user_folder_key_type', 1), + user_data_key, + user_rsa_key, + user_ecc_key + ) + + if forbid_rsa: + encrypted_key, key_type = self._encrypt_for_target(uf_key, target_keys) + else: + if target_keys.aes: + encrypted_key = crypto.encrypt_aes_v1(uf_key, target_keys.aes) + key_type = EncryptionType.ENCRYPTED_BY_DATA_KEY.value + elif target_keys.rsa: + rsa_key = crypto.load_rsa_public_key(target_keys.rsa) + encrypted_key = crypto.encrypt_rsa(uf_key, rsa_key) + key_type = EncryptionType.ENCRYPTED_BY_PUBLIC_KEY.value + else: + raise Exception('No valid target key for user folder') + + reencrypted.append({ + 'user_folder_uid': ufk['user_folder_uid'], + 'user_folder_key': utils.base64_url_encode(encrypted_key), + 'user_folder_key_type': key_type + }) + except Exception as e: + logger.debug(f"Corrupted UF key {ufk.get('user_folder_uid', 'unknown')}: {e}") + corrupted.append(ufk) + + return reencrypted, corrupted, transfer_folder + + def _decrypt_key_by_type(self, + encrypted_key: bytes, + key_type: int, + user_data_key: bytes, + user_rsa_key: Optional[Any], + user_ecc_key: Optional[Any]) -> bytes: + + if key_type == TransferKeyType.RAW_DATA_KEY.value: + return user_data_key + elif key_type == TransferKeyType.ENCRYPTED_BY_DATA_KEY.value: + return crypto.decrypt_aes_v1(encrypted_key, user_data_key) + elif key_type == TransferKeyType.ENCRYPTED_BY_RSA.value: + if not user_rsa_key: + raise Exception('RSA private key required but not available') + return crypto.decrypt_rsa(encrypted_key, user_rsa_key) + elif key_type == TransferKeyType.ENCRYPTED_BY_DATA_KEY_GCM.value: + return crypto.decrypt_aes_v2(encrypted_key, user_data_key) + elif key_type == TransferKeyType.ENCRYPTED_BY_ECC.value: + if not user_ecc_key: + raise Exception('ECC private key required but not available') + return crypto.decrypt_ec(encrypted_key, user_ecc_key) + else: + raise Exception(f'Unsupported key type: {key_type}') + + def _encrypt_for_target(self, + key: bytes, + target_keys: keeper_auth.UserKeys) -> Tuple[bytes, str]: + + if target_keys.aes: + return (crypto.encrypt_aes_v2(key, target_keys.aes), + EncryptionType.ENCRYPTED_BY_DATA_KEY_GCM.value) + elif target_keys.ec: + ec_key = crypto.load_ec_public_key(target_keys.ec) + return (crypto.encrypt_ec(key, ec_key), + EncryptionType.ENCRYPTED_BY_PUBLIC_KEY_ECC.value) + elif target_keys.rsa and not self.auth.auth_context.forbid_rsa: + rsa_key = crypto.load_rsa_public_key(target_keys.rsa) + return (crypto.encrypt_rsa(key, rsa_key), + EncryptionType.ENCRYPTED_BY_PUBLIC_KEY.value) + else: + raise Exception('No valid target public key available') + + def _execute_transfer(self, + from_username: str, + to_username: str, + transfer_data: Dict) -> TransferResult: + + rq = { + 'command': 'transfer_and_delete_user', + 'from_user': from_username, + 'to_user': to_username + } + + transfer_keys = [key.value for key in TransferDataKeys] + for key in transfer_keys: + if key in transfer_data: + rq[key] = transfer_data[key] + + logger.info(f'Executing transfer_and_delete_user API: from={from_username} to={to_username}') + if 'user_folder_transfer' in transfer_data: + folder_transfer = transfer_data['user_folder_transfer'] + logger.info(f'Creating transfer folder: {folder_transfer.get("transfer_folder_uid", "unknown_uid")}') + + self.auth.execute_auth_command(rq) + + return TransferResult( + success=True, + username=from_username, + records_transferred=len(transfer_data.get('record_keys', [])), + shared_folders_transferred=len(transfer_data.get('shared_folder_keys', [])), + teams_transferred=len(transfer_data.get('team_keys', [])), + user_folders_transferred=len(transfer_data.get('user_folder_keys', [])), + corrupted_records=len(transfer_data.get('corrupted_record_keys', [])), + corrupted_shared_folders=len(transfer_data.get('corrupted_shared_folder_keys', [])), + corrupted_teams=len(transfer_data.get('corrupted_team_keys', [])), + corrupted_user_folders=len(transfer_data.get('corrupted_user_folder_keys', [])) + ) + From d7917eae6638b193d2c7a0307cf5e9b4e5fdb6a3 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 28 Oct 2025 13:53:59 +0530 Subject: [PATCH 40/44] Record permission command added --- .../commands/record_handling_commands.py | 663 +++++++++++++++++- .../src/keepercli/register_commands.py | 1 + 2 files changed, 663 insertions(+), 1 deletion(-) diff --git a/keepercli-package/src/keepercli/commands/record_handling_commands.py b/keepercli-package/src/keepercli/commands/record_handling_commands.py index 966aad69..33ea20fc 100644 --- a/keepercli-package/src/keepercli/commands/record_handling_commands.py +++ b/keepercli-package/src/keepercli/commands/record_handling_commands.py @@ -8,7 +8,7 @@ from colorama import Fore, Back, Style -from keepersdk.proto import record_pb2 +from keepersdk.proto import record_pb2, folder_pb2 from keepersdk.vault import (record_types, vault_record, vault_online, record_management) from keepersdk import crypto, utils @@ -1106,3 +1106,664 @@ def _format_url(self, url, include_in_output): parsed_url = parsed_url[:URL_DISPLAY_LENGTH] if parsed_url else '' return [parsed_url] if include_in_output else [] + +class _PermissionConfig: + """Configuration for permission changes. + + Attributes: + should_have: True if granting permissions, False if revoking + change_share: Whether to change share permissions + change_edit: Whether to change edit permissions + force: Skip confirmation prompts + dry_run: Only display changes without applying them + recursive: Apply to subfolders + """ + def __init__(self, action: str, can_share: bool, can_edit: bool, + force: bool, dry_run: bool, recursive: bool): + self.should_have = action == 'grant' + self.change_share = can_share + self.change_edit = can_edit + self.force = force + self.dry_run = dry_run + self.recursive = recursive + + if not self.change_share and not self.change_edit: + raise base.CommandError( + 'Please choose at least one of the following options: can-edit, can-share' + ) + + +class _PermissionProcessor: + """Handles processing of permission changes for records.""" + + def __init__(self, config: _PermissionConfig, context: KeeperParams): + self.config = config + self.context = context + self.vault = context.vault + + def process_direct_shares(self, folders): + """Process direct record shares and return commands to update.""" + updates = [] + skipped = [] + + record_uids = set() + for folder in folders: + if folder.records: + record_uids.update(folder.records) + + if not record_uids: + return updates, skipped + + shared_records = share_utils.get_record_shares(self.vault, record_uids) + if not shared_records: + return updates, skipped + + for shared_record in shared_records: + shares = shared_record.get('shares', {}) + user_permissions = shares.get('user_permissions', []) + + for up in user_permissions: + if up.get('owner'): # Skip record owners + continue + + username = up.get('username') + if username == self.context.username: # Skip self + continue + + needs_update = self._needs_permission_update( + up, self.config.should_have, self.config.change_share, self.config.change_edit + ) + + if needs_update: + updates.append({ + 'record_uid': shared_record.get('record_uid'), + 'to_username': username, + 'editable': self.config.should_have if self.config.change_edit else up.get('editable'), + 'shareable': self.config.should_have if self.config.change_share else up.get('shareable'), + }) + + return updates, skipped + + def process_shared_folder_permissions(self, folders): + """Process shared folder record permissions and return commands to update.""" + updates = {} + skipped = {} + + share_admin_folders = self._get_share_admin_folders(folders) + + account_uid = self.context.auth.auth_context.account_uid + + for folder in folders: + if folder.folder_type not in ['shared_folder', 'shared_folder_folder']: + continue + + shared_folder_uid = self._get_shared_folder_uid(folder) + if not shared_folder_uid or shared_folder_uid not in self.vault.vault_data._shared_folders: + continue + + is_share_admin = shared_folder_uid in share_admin_folders + shared_folder = self.vault.vault_data.load_shared_folder(shared_folder_uid) + + has_manage_records = self._has_manage_records_permission( + shared_folder, shared_folder_uid, is_share_admin, account_uid + ) + + container = updates if (is_share_admin or has_manage_records) else skipped + + if shared_folder.record_permissions: + record_uids = folder.records if folder.records else set() + for rp in shared_folder.record_permissions: + record_uid = rp.record_uid + if record_uid in record_uids and record_uid not in container.get(shared_folder_uid, {}): + if self._needs_shared_folder_update(rp): + container.setdefault(shared_folder_uid, {}) + container[shared_folder_uid][record_uid] = self._build_update_command( + record_uid, shared_folder_uid + ) + + return self._clean_empty_dicts(updates), self._clean_empty_dicts(skipped) + + def _needs_permission_update(self, user_perm, should_have, change_share, change_edit): + """Check if user permission needs updating.""" + if change_edit and should_have != user_perm.get('editable'): + return True + if change_share and should_have != user_perm.get('shareable'): + return True + return False + + def _needs_shared_folder_update(self, record_permission): + """Check if shared folder record permission needs updating.""" + should_have = self.config.should_have + if self.config.change_edit and should_have != record_permission.can_edit: + return True + if self.config.change_share and should_have != record_permission.can_share: + return True + return False + + def _get_share_admin_folders(self, folders): + """Get set of shared folder UIDs where user is share admin.""" + share_admin_folders = set() + shared_folder_uids = set() + + for folder in folders: + shared_folder_uid = None + if folder.folder_type == 'shared_folder': + shared_folder_uid = folder.folder_uid + elif folder.folder_type == 'shared_folder_folder': + shared_folder_uid = folder.folder_scope_uid + + if shared_folder_uid and shared_folder_uid not in shared_folder_uids: + if shared_folder_uid in self.vault.vault_data._shared_folders: + shared_folder_uids.add(shared_folder_uid) + + if not shared_folder_uids: + return share_admin_folders + + try: + rq = record_pb2.AmIShareAdmin() + for shared_folder_uid in shared_folder_uids: + osa = record_pb2.IsObjectShareAdmin() + osa.uid = utils.base64_url_decode(shared_folder_uid) + osa.objectType = record_pb2.CHECK_SA_ON_SF + rq.isObjectShareAdmin.append(osa) + + rs = self.vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/am_i_share_admin', + request=rq, + response_type=record_pb2.AmIShareAdmin + ) + + for osa in rs.isObjectShareAdmin: + if osa.isAdmin: + share_admin_folders.add(utils.base64_url_encode(osa.uid)) + except Exception: + pass + + return share_admin_folders + + def _get_shared_folder_uid(self, folder): + """Get the shared folder UID from a folder object.""" + if folder.folder_type == 'shared_folder': + return folder.folder_uid + elif folder.folder_type == 'shared_folder_folder': + return folder.folder_scope_uid + return None + + def _has_manage_records_permission(self, shared_folder, shared_folder_uid, is_share_admin, account_uid): + """Check if user has permission to manage records in shared folder.""" + if is_share_admin: + return True + + if shared_folder.user_permissions: + if shared_folder.user_permissions[0].user_uid == account_uid: + return True + + user = next( + (x for x in shared_folder.user_permissions if x.name == self.context.username), + None + ) + if user and user.manage_records: + return True + + return False + + def _build_update_command(self, record_uid, shared_folder_uid): + """Build a protobuf command to update record permissions.""" + cmd = folder_pb2.SharedFolderUpdateRecord() + cmd.recordUid = utils.base64_url_decode(record_uid) + cmd.sharedFolderUid = utils.base64_url_decode(shared_folder_uid) + + cmd.canEdit = ( + folder_pb2.BOOLEAN_TRUE if self.config.should_have else folder_pb2.BOOLEAN_FALSE + ) if self.config.change_edit else folder_pb2.BOOLEAN_NO_CHANGE + + cmd.canShare = ( + folder_pb2.BOOLEAN_TRUE if self.config.should_have else folder_pb2.BOOLEAN_FALSE + ) if self.config.change_share else folder_pb2.BOOLEAN_NO_CHANGE + + return cmd + + @staticmethod + def _clean_empty_dicts(data): + """Remove empty dictionaries from nested structure.""" + cleaned = {} + for key, value in data.items(): + if isinstance(value, dict) and value: + cleaned[key] = value + return cleaned + + +class _PermissionReporter: + """Handles reporting of permission changes.""" + + def __init__(self, config: _PermissionConfig, context: KeeperParams): + self.config = config + self.context = context + self.vault = context.vault + + def report_direct_shares(self, updates, skipped): + """Report on direct share updates and skipped items.""" + if skipped and self.config.dry_run: + self._report_skipped_direct_shares(skipped) + + if updates and not self.config.force: + self._report_direct_share_updates(updates) + + def report_shared_folder_changes(self, updates, skipped): + """Report on shared folder updates and skipped items.""" + if skipped and self.config.dry_run: + self._report_skipped_shared_folder(skipped) + + if updates and not self.config.force: + self._report_shared_folder_updates(updates) + + def _report_skipped_direct_shares(self, skipped): + """Report records that couldn't be updated due to insufficient permissions.""" + table = [] + for cmd in skipped: + record_uid = utils.base64_url_encode(cmd['recordUid']) + record = self.vault.vault_data.get_record(record_uid=record_uid) + record_owners = [x['username'] for x in record['shares']['user_permissions'] if x['owner']] + record_owner = record_owners[0] if len(record_owners) > 0 else '' + rec = self.vault.vault_data.get_record(record_uid=record_uid) + row = [record_uid, rec.title[:32], record_owner, cmd['to_username']] + table.append(row) + + headers = ['Record UID', 'Title', 'Owner', 'Email'] + title = 'SKIP Direct Record Share permission(s). Not permitted' + report_utils.dump_report_data(table, headers, title=title, row_number=True, group_by=0) + logger.info('\n') + + def _report_direct_share_updates(self, updates): + """Report direct share updates that will be made.""" + table = [] + for cmd in updates: + record_uid = cmd['record_uid'] + rec = self.vault.vault_data.get_record(record_uid=record_uid) + row = [record_uid, rec.title[:32], cmd['to_username']] + + if self.config.change_edit: + row.append('Y' if cmd['editable'] else 'N') + if self.config.change_share: + row.append('Y' if cmd['shareable'] else 'N') + + table.append(row) + + headers = ['Record UID', 'Title', 'Email'] + if self.config.change_edit: + headers.append('Can Edit') + if self.config.change_share: + headers.append('Can Share') + + action = 'GRANT' if self.config.should_have else 'REVOKE' + title = f'{action} Direct Record Share permission(s)' + report_utils.dump_report_data(table, headers, title=title, row_number=True, group_by=0) + logger.info('\n') + + def _report_skipped_shared_folder(self, skipped): + """Report shared folder records that couldn't be updated.""" + table = [] + for shared_folder_uid in skipped: + shared_folder = self.vault.vault_data.get_shared_folder(shared_folder_uid=shared_folder_uid) + uid = shared_folder_uid + name = shared_folder.name[:32] + + for record_uid in skipped[shared_folder_uid]: + record = self.vault.vault_data.get_record(record_uid=record_uid) + row = [uid, name, record_uid, record.title[:32]] + uid = '' + name = '' + table.append(row) + + if table: + headers = ['Shared Folder UID', 'Shared Folder Name', 'Record UID', 'Record Title'] + title = 'SKIP Shared Folder Record Share permission(s). Not permitted' + report_utils.dump_report_data(table, headers, title=title, row_number=True) + logger.info('\n') + + def _report_shared_folder_updates(self, updates): + """Report shared folder updates that will be made.""" + table = [] + for shared_folder_uid in updates: + commands = updates[shared_folder_uid] + shared_folder = self.vault.vault_data.get_shared_folder(shared_folder_uid=shared_folder_uid) + uid = shared_folder_uid + name = shared_folder.name[:32] + + for record_uid in commands: + cmd = commands[record_uid] + record = self.vault.vault_data.get_record(record_uid=record_uid) + row = [uid, name, record_uid, record.title[:32]] + + if self.config.change_edit: + edit_val = 'Y' if cmd.canEdit == folder_pb2.BOOLEAN_TRUE else 'N' + row.append(edit_val) + if self.config.change_share: + share_val = 'Y' if cmd.canShare == folder_pb2.BOOLEAN_TRUE else 'N' + row.append(share_val) + + table.append(row) + uid = '' + name = '' + + if table: + headers = ['Shared Folder UID', 'Shared Folder Name', 'Record UID', 'Record Title'] + if self.config.change_edit: + headers.append('Can Edit') + if self.config.change_share: + headers.append('Can Share') + + action = 'GRANT' if self.config.should_have else 'REVOKE' + title = f'{action} Shared Folder Record Share permission(s)' + report_utils.dump_report_data(table, headers, title=title, row_number=True) + logger.info('\n') + + +class _PermissionExecutor: + """Handles execution of permission changes.""" + + def __init__(self, config: _PermissionConfig, context: KeeperParams): + self.config = config + self.context = context + self.vault = context.vault + + def execute_direct_share_updates(self, updates): + """Execute direct share permission updates.""" + if not updates: + return [] + + errors = [] + batch_size = 900 + + while updates: + batch = updates[:batch_size] + updates = updates[batch_size:] + + rsu_rq = record_pb2.RecordShareUpdateRequest() + rsu_rq.updateSharedRecord.extend((self._to_share_record_proto(x) for x in batch)) + + rsu_rs = self.vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/records_share_update', + request=rsu_rq, + response_type=record_pb2.RecordShareUpdateResponse + ) + + for status in rsu_rs.updateSharedRecordStatus: + if status.status.lower() != 'success': + record_uid = utils.base64_url_encode(status.recordUid) + errors.append([record_uid, status.username, status.status.lower(), status.message]) + + return errors + + def execute_shared_folder_updates(self, updates): + """Execute shared folder permission updates.""" + if not updates: + return [] + + errors = [] + requests = self._build_shared_folder_requests(updates) + chunks = self._chunk_requests(requests) + + for chunk in chunks: + rqs = folder_pb2.SharedFolderUpdateV3RequestV2() + rqs.sharedFoldersUpdateV3.extend(chunk.values()) + + rss = self.vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/shared_folder_update_v3', + request=rqs, + response_type=folder_pb2.SharedFolderUpdateV3ResponseV2, + payload_version=1 + ) + + for rs in rss.sharedFoldersUpdateV3Response: + shared_folder_uid = utils.base64_url_encode(rs.sharedFolderUid) + for status in rs.sharedFolderUpdateRecordStatus: + if status.status != 'success': + record_uid = utils.base64_url_encode(status.recordUid) + errors.append([shared_folder_uid, record_uid, status.status]) + + return errors + + def _build_shared_folder_requests(self, updates): + """Build protobuf requests for shared folder updates.""" + requests = [] + + for shared_folder_uid in updates: + update_commands = list(updates[shared_folder_uid].values()) + batch_size = 490 + + while update_commands: + batch = update_commands[:batch_size] + update_commands = update_commands[batch_size:] + + rq = folder_pb2.SharedFolderUpdateV3Request() + rq.sharedFolderUid = utils.base64_url_decode(shared_folder_uid) + rq.forceUpdate = True + rq.sharedFolderUpdateRecord.extend(batch) + if batch: + rq.fromTeamUid = batch[0].teamUid + requests.append(rq) + + return requests + + def _chunk_requests(self, requests): + """Chunk requests to stay within size limits.""" + chunks = [] + current_chunk = {} + total_elements = 0 + + for rq in requests: + if rq.sharedFolderUid in current_chunk: + chunks.append(current_chunk) + current_chunk = {} + total_elements = 0 + + batch_size = len(rq.sharedFolderUpdateRecord) + if total_elements + batch_size > 500: + chunks.append(current_chunk) + current_chunk = {} + total_elements = 0 + + current_chunk[rq.sharedFolderUid] = rq + total_elements += batch_size + + if current_chunk: + chunks.append(current_chunk) + + return chunks + + def _to_share_record_proto(self, srd): + """Convert dictionary to SharedRecord protobuf.""" + srp = record_pb2.SharedRecord() + srp.toUsername = srd['to_username'] + srp.recordUid = utils.base64_url_decode(srd['record_uid']) + + if 'shared_folder_uid' in srd: + srp.sharedFolderUid = utils.base64_url_decode(srd['shared_folder_uid']) + if 'team_uid' in srd: + srp.teamUid = utils.base64_url_decode(srd['team_uid']) + if 'record_key' in srd: + srp.recordKey = utils.base64_url_decode(srd['record_key']) + if 'use_ecc_key' in srd: + srp.useEccKey = srd['use_ecc_key'] + if 'editable' in srd: + srp.editable = srd['editable'] + if 'shareable' in srd: + srp.shareable = srd['shareable'] + if 'transfer' in srd: + srp.transfer = srd['transfer'] + + return srp + + +class RecordPermissionCommand(base.ArgparseCommand): + """Command to modify record permissions in folders and shared folders.""" + + SHARED_FOLDER = 'shared_folder' + SHARED_FOLDER_FOLDER = 'shared_folder_folder' + SHARED_FOLDER_TYPES = [SHARED_FOLDER, SHARED_FOLDER_FOLDER] + + def __init__(self): + parser = argparse.ArgumentParser(prog='record-permission', description='Modify the permissions of a record') + RecordPermissionCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--dry-run', dest='dry_run', action='store_true', + help='Display the permissions changes without committing them') + parser.add_argument('--force', dest='force', action='store_true', + help='Apply permission changes without any confirmation') + parser.add_argument('-R', '--recursive', dest='recursive', action='store_true', + help='Apply permission changes to all sub-folders') + parser.add_argument('--share-record', dest='share_record', action='store_true', + help='Change a records sharing permissions') + parser.add_argument('--share-folder', dest='share_folder', action='store_true', + help='Change a folders sharing permissions') + parser.add_argument('-a', '--action', dest='action', action='store', choices=['grant', 'revoke'], + required=True, help='The action being taken') + parser.add_argument('-s', '--can-share', dest='can_share', action='store_true', + help='Set record permission: can be shared') + parser.add_argument('-d', '--can-edit', dest='can_edit', action='store_true', + help='Set record permission: can be edited') + parser.add_argument('folder', nargs='?', type=str, action='store', help='folder path or folder UID') + parser.error = base.ArgparseCommand.raise_parse_exception + parser.exit = base.ArgparseCommand.suppress_exit + + def _resolve_folder(self, context, folder_name): + """Resolve folder from name or UID.""" + vault = context.vault + + if not folder_name: + return vault.vault_data.root_folder + + if folder_name in vault.vault_data._folders: + return vault.vault_data.get_folder(folder_name) + + folder, path = folder_utils.try_resolve_path(context, folder_name) + if len(path) == 0: + return folder + + raise base.CommandError(f'Folder {folder_name} not found') + + def _get_folders_to_process(self, start_folder, recursive): + """Get list of folders to process, optionally recursively.""" + folders = [start_folder] + + if not recursive: + return folders + + visited = {start_folder.folder_uid} + pos = 0 + + while pos < len(folders): + folder = folders[pos] + if folder.subfolders: + for subfolder_uid in folder.subfolders: + if subfolder_uid not in visited: + subfolder = self.vault.vault_data.get_folder(subfolder_uid) + if subfolder: + folders.append(subfolder) + visited.add(subfolder_uid) + pos += 1 + + logger.debug('Folder count: %s', len(folders)) + return folders + + def _determine_scope(self, kwargs): + """Determine if processing share_record, share_folder, or both.""" + share_record = kwargs.get('share_record', False) + share_folder = kwargs.get('share_folder', False) + + if not share_record and not share_folder: + return True, True + + return share_record, share_folder + + def _log_permission_request(self, folder, config): + """Log the permission change request.""" + if config.force: + return + + action = 'GRANT' if config.should_have else 'REVOKE' + scope = ['recursively' if config.recursive else 'only'] + + permissions = [] + if config.change_edit: + permissions.append('"Can Edit"') + if config.change_share: + permissions.append('"Can Share"') + + permission_str = ' & '.join(permissions) + logger.info( + f'\nRequest to {action} {permission_str} permission(s) in "{folder.name}" folder {scope[0]}' + ) + + def execute(self, context: KeeperParams, **kwargs): + """Execute record permission changes.""" + if not context.vault: + raise base.CommandError('Vault is not initialized') + + self.vault = context.vault + + config = _PermissionConfig( + action=kwargs.get('action', ''), + can_share=kwargs.get('can_share', False), + can_edit=kwargs.get('can_edit', False), + force=kwargs.get('force', False), + dry_run=kwargs.get('dry_run', False), + recursive=kwargs.get('recursive', False) + ) + + folder = self._resolve_folder(context, kwargs.get('folder', '')) + folders = self._get_folders_to_process(folder, config.recursive) + + share_record, share_folder = self._determine_scope(kwargs) + + self._log_permission_request(folder, config) + + processor = _PermissionProcessor(config, context) + reporter = _PermissionReporter(config, context) + executor = _PermissionExecutor(config, context) + + direct_share_updates = [] + direct_share_skipped = [] + shared_folder_updates = {} + shared_folder_skipped = {} + + if share_record: + direct_share_updates, direct_share_skipped = processor.process_direct_shares(folders) + + if share_folder: + shared_folder_updates, shared_folder_skipped = processor.process_shared_folder_permissions(folders) + + reporter.report_direct_shares(direct_share_updates, direct_share_skipped) + reporter.report_shared_folder_changes(shared_folder_updates, shared_folder_skipped) + + if not config.dry_run and (direct_share_updates or shared_folder_updates): + if not config.force: + answer = prompt_utils.user_choice( + "Do you want to proceed with these permission changes?", 'yn', 'n' + ) + if answer.lower() != 'y': + return + + if direct_share_updates: + direct_errors = executor.execute_direct_share_updates(direct_share_updates) + if direct_errors: + headers = ['Record UID', 'Email', 'Error Code', 'Message'] + action = 'GRANT' if config.should_have else 'REVOKE' + title = f'Failed to {action} Direct Record Share permission(s)' + report_utils.dump_report_data(direct_errors, headers, title=title, row_number=True) + logger.info('\n') + + if shared_folder_updates: + shared_folder_errors = executor.execute_shared_folder_updates(shared_folder_updates) + if shared_folder_errors: + headers = ['Shared Folder UID', 'Record UID', 'Error Code'] + action = 'GRANT' if config.should_have else 'REVOKE' + title = f'Failed to {action} Shared Folder Record Share permission(s)' + report_utils.dump_report_data(shared_folder_errors, headers, title=title) + logger.info('\n') + + self.vault.sync_down(True) diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index d4b52c70..510cf021 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -74,6 +74,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('share-list', share_management.OneTimeShareListCommand(), base.CommandScope.Vault) commands.register_command('share-create', share_management.OneTimeShareCreateCommand(), base.CommandScope.Vault) commands.register_command('share-remove', share_management.OneTimeShareRemoveCommand(), base.CommandScope.Vault) + commands.register_command('record-permission', record_handling_commands.RecordPermissionCommand(), base.CommandScope.Vault) commands.register_command('trash', trash.TrashCommand(), base.CommandScope.Vault) From f514c475db4b22d6beaa68ae8d6d07188cfe1f80 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 30 Oct 2025 16:08:04 +0530 Subject: [PATCH 41/44] Added examples of user and record related command --- examples/enterprise_user/create_user.py | 191 ++++++++++++++++++++ examples/enterprise_user/transfer_user.py | 132 ++++++++++++++ examples/record/find_duplicate.py | 183 +++++++++++++++++++ examples/record/share_record_permissions.py | 187 +++++++++++++++++++ 4 files changed, 693 insertions(+) create mode 100644 examples/enterprise_user/create_user.py create mode 100644 examples/enterprise_user/transfer_user.py create mode 100644 examples/record/find_duplicate.py create mode 100644 examples/record/share_record_permissions.py diff --git a/examples/enterprise_user/create_user.py b/examples/enterprise_user/create_user.py new file mode 100644 index 00000000..c06ff9be --- /dev/null +++ b/examples/enterprise_user/create_user.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def create_enterprise_user( + context: KeeperParams, + email: str, + full_name: Optional[str] = None, + parent_node: Optional[str] = None, + job_title: Optional[str] = None, + add_roles: Optional[list] = None, + add_teams: Optional[list] = None, + hide_shared_folders: Optional[str] = None +): + """ + Create a new enterprise user. + + This function creates a new enterprise user using the Keeper CLI + `EnterpriseUserAddCommand` and returns True if successful. + """ + try: + create_command = EnterpriseUserAddCommand() + + kwargs = { + 'email': [email] # EnterpriseUserAddCommand expects a list + } + + if full_name: + kwargs['full_name'] = full_name + if parent_node: + kwargs['parent'] = parent_node + if job_title: + kwargs['job_title'] = job_title + if add_roles: + kwargs['add_role'] = add_roles + if add_teams: + kwargs['add_team'] = add_teams + if hide_shared_folders: + kwargs['hide_shared_folders'] = hide_shared_folders + + print(f'Creating enterprise user: {email}') + if full_name: + print(f'Full name: {full_name}') + if parent_node: + print(f'Parent node: {parent_node}') + if job_title: + print(f'Job title: {job_title}') + if add_roles: + print(f'Roles: {", ".join(add_roles)}') + if add_teams: + print(f'Teams: {", ".join(add_teams)}') + if hide_shared_folders: + print(f'Hide shared folders: {hide_shared_folders}') + + # Execute the create user command + create_command.execute(context=context, **kwargs) + + print(f'\nEnterprise user created successfully!') + print(f'Email: {email}') + return True + + except Exception as e: + print(f'Error creating enterprise user: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Create an enterprise user using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python create_user.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Configuration constants - modify these values as needed + email = 'testuser@example.com' + full_name = 'Test User' + parent_node = 'TestNode' + job_title = 'Test Employee' + add_roles = None # Example: ['Admin', 'Manager'] + add_teams = None # Example: ['IT Team', 'Security Team'] + hide_shared_folders = None # Options: 'on', 'off', or None + + # Validate email format (basic check) + if '@' not in email: + print('Error: Invalid email address format') + sys.exit(1) + + # Validate hide-shared-folders usage + if hide_shared_folders and not add_teams: + print('Warning: hide_shared_folders only works with add_teams') + + print(f'Using test email: {email}') + print(f'Using parent node: {parent_node}') + print(f'Using full name: {full_name}') + print(f'Using job title: {job_title}') + + context = None + try: + context = login_to_keeper_with_config(args.config) + + # Ensure enterprise data is loaded + if not context.enterprise_data: + print('Loading enterprise data...') + context.enterprise_loader.load() + + success = create_enterprise_user( + context=context, + email=email, + full_name=full_name, + parent_node=parent_node, + job_title=job_title, + add_roles=add_roles, + add_teams=add_teams, + hide_shared_folders=hide_shared_folders + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/enterprise_user/transfer_user.py b/examples/enterprise_user/transfer_user.py new file mode 100644 index 00000000..474d9f0b --- /dev/null +++ b/examples/enterprise_user/transfer_user.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file and return an authenticated context. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Transfer user accounts using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python transfer_user.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Configuration constants - modify these values as needed + # Bool flags can be set to True or None (to be sent as False) + source_users = ['testuser@example.com'] # List of source user emails + target_user = 'admin@example.com' # Target user email + mapping_file = None # Path to existing mapping file (overrides other options if set) + force = None # Set to True to skip confirmation prompts + + # Validate email formats (basic check) + all_emails = source_users + [target_user] if target_user else source_users + for email in all_emails: + if '@' not in email: + print(f'Error: Invalid email address format: {email}') + sys.exit(1) + + # Check for self-transfer + if target_user and target_user in source_users: + print('Error: Target user cannot be the same as source user') + sys.exit(1) + + # Display test configuration + print(f'Using source users: {", ".join(source_users)}') + print(f'Using target user: {target_user}') + print(f'Force mode: {force}') + + context = None + try: + context = login_to_keeper_with_config(args.config) + + # Ensure enterprise data is loaded + if not context.enterprise_data: + print('Loading enterprise data...') + context.enterprise_loader.load() + + # Prepare command invocation + transfer_command = EnterpriseTransferAccountCommand() + if mapping_file: + if not os.path.exists(mapping_file): + print(f'Error: Mapping file {mapping_file} not found') + sys.exit(1) + print(f'Using external mapping file: {mapping_file}') + transfer_command.execute( + context=context, + email=[f'@{mapping_file}'], + force=force + ) + else: + transfer_command.execute( + context=context, + email=source_users, + target_user=target_user, + force=force + ) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/record/find_duplicate.py b/examples/record/find_duplicate.py new file mode 100644 index 00000000..aaef8da1 --- /dev/null +++ b/examples/record/find_duplicate.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def find_duplicate_records( + context: KeeperParams, + match_by_title: Optional[bool] = None, + match_by_login: Optional[bool] = None, + match_by_password: Optional[bool] = None, + match_by_url: Optional[bool] = None, + match_by_shares: Optional[bool] = None, + match_full: Optional[bool] = None, + quiet: Optional[bool] = None, + output_format: Optional[str] = None, + merge: Optional[bool] = None, + ignore_shares_on_merge: Optional[bool] = None, + force: Optional[bool] = None, + dry_run: Optional[bool] = None, + scope: Optional[str] = None, + refresh_data: Optional[bool] = None, + output: Optional[str] = None +): + """ + Find duplicate records in the vault based on specified criteria using + the Keeper CLI FindDuplicateCommand implementation. + """ + try: + if not context.vault: + raise Exception('Vault is not initialized') + + print('Finding duplicate records via FindDuplicateCommand...') + + + kwargs = { + 'title': match_by_title, + 'login': match_by_login, + 'password': match_by_password, + 'url': match_by_url, + 'shares': match_by_shares, + 'full': match_full, + 'merge': merge, + 'ignore_shares_on_merge': ignore_shares_on_merge, + 'force': force, + 'dry_run': dry_run, + 'quiet': quiet, + 'scope': scope, + 'refresh_data': refresh_data, + 'format': output_format, + 'output': output, + } + + cmd = FindDuplicateCommand() + cmd.execute(context, **kwargs) + print('\nFind duplicate operation completed successfully') + return True + + except Exception as e: + print(f'Error finding duplicate records: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Find duplicate records in the vault using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python find_duplicate.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Configuration constants - modify these values as needed + # Boolean flags can be set to True or None (to be sent as False) + match_by_title = True # Match duplicates by title + match_by_login = None # Match duplicates by login + match_by_password = None # Match duplicates by password + match_by_url = None # Match duplicates by URL + match_by_shares = None # Match duplicates by share permissions + match_full = None # Match duplicates by all fields (overrides individual settings) + output_format = None # Options: 'table', 'csv', 'json' + quiet = None # Set to True to suppress warning messages during processing + merge = None # Set to True to merge duplicates + ignore_shares_on_merge = None # Set to True to ignore share permissions when merging duplicates + force = None # Set to True to force the operation + dry_run = None # Set to True to simulate the operation + scope = None # Set the scope of the search + refresh_data = None # Set to True to refresh the data + output = None # Set the output format + + context = None + try: + context = login_to_keeper_with_config(args.config) + success = find_duplicate_records( + context=context, + match_by_title=match_by_title, + match_by_login=match_by_login, + match_by_password=match_by_password, + match_by_url=match_by_url, + match_by_shares=match_by_shares, + match_full=match_full, + quiet=quiet, + output_format=output_format, + merge=merge, + ignore_shares_on_merge=ignore_shares_on_merge, + force=force, + dry_run=dry_run, + scope=scope, + refresh_data=refresh_data, + output=output, + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/record/share_record_permissions.py b/examples/record/share_record_permissions.py new file mode 100644 index 00000000..8bc255ab --- /dev/null +++ b/examples/record/share_record_permissions.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def manage_record_permissions( + context: KeeperParams, + folder: Optional[str] = None, + action: str = 'grant', + can_edit: Optional[bool] = None, + can_share: Optional[bool] = None, + force: Optional[bool] = None, + dry_run: Optional[bool] = None, + recursive: Optional[bool] = None, + share_record: Optional[bool] = None, + share_folder: Optional[bool] = None +): + """ + Manage record permissions using RecordPermissionCommand. + + This adjusts the "Can Edit" and/or "Can Share" flags on records within a + folder (optionally recursively). You can scope the change to direct record + shares (share_record), shared folder shares (share_folder), or both. + """ + try: + cmd = RecordPermissionCommand() + + kwargs = { + 'folder': folder or '', + 'action': action, + 'can_edit': can_edit, + 'can_share': can_share, + 'force': force, + 'dry_run': dry_run, + 'recursive': recursive, + 'share_record': share_record, + 'share_folder': share_folder, + } + + print('Managing record permissions...') + print(f'Folder: {folder or ""}') + print(f'Action: {action}') + print(f'Can edit: {can_edit}') + print(f'Can share: {can_share}') + print(f'Scope - share_record: {share_record}, share_folder: {share_folder}') + print(f'Force: {force}') + print(f'Dry run: {dry_run}') + print(f'Recursive: {recursive}') + + if dry_run: + print('\nDRY RUN MODE: No changes will be made') + + cmd.execute(context=context, **kwargs) + + print('\nRecord permission operation completed successfully!') + return True + + except Exception as e: + print(f'Error managing record permissions: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Manage record permissions using RecordPermissionCommand', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Example: + python share_record_permissions.py + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + print(f'Config file {args.config} not found') + sys.exit(1) + + # Configuration constants - modify these values as needed + # Bool flags can be set to True or None (to be sent as False) + folder = "folder_uid_or_path" # Folder path or folder UID (None for root) + action = 'grant' # 'grant' or 'revoke' + can_edit = True # Set "Can Edit" permission + can_share = None # Set "Can Share" permission + share_record = None # Modify direct record shares + share_folder = None # Modify shared folder record shares + recursive = None # Apply to sub-folders + force = None # Apply changes without confirmation + dry_run = None # Show changes without applying + + # Display selected configuration + print(f'Folder: {folder or ""}') + print(f'Action: {action}') + summary = [] + if can_edit: + summary.append('edit') + if can_share: + summary.append('share') + if not summary: + summary.append('no-change') + print(f'Permissions: {", ".join(summary)}') + print(f'Scope - share_record: {share_record}, share_folder: {share_folder}') + if dry_run: + print('Mode: DRY RUN (no changes will be made)') + + context = None + try: + context = login_to_keeper_with_config(args.config) + + success = manage_record_permissions( + context=context, + folder=folder, + action=action, + can_edit=can_edit, + can_share=can_share, + force=force, + dry_run=dry_run, + recursive=recursive, + share_record=share_record, + share_folder=share_folder, + ) + + if not success: + sys.exit(1) + + except Exception as e: + print(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() From 41cdce1c979731e9e3fac9da0e5bd3eae2d732e7 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 31 Oct 2025 17:44:44 +0530 Subject: [PATCH 42/44] Device approve command added --- README.md | 11 +- .../src/keepercli/commands/enterprise_user.py | 456 +++++++++++++++++- .../src/keepercli/register_commands.py | 3 +- .../keepersdk/enterprise/enterprise_data.py | 6 + .../keepersdk/enterprise/enterprise_types.py | 23 + .../src/keepersdk/enterprise/private_data.py | 29 ++ 6 files changed, 521 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 676217af..5a1262cf 100644 --- a/README.md +++ b/README.md @@ -139,12 +139,15 @@ Below is a complete example demonstrating authentication, vault synchronization, ```python import sqlite3 +import getpass + from keepersdk.authentication import login_auth, configuration, endpoint from keepersdk.vault import sqlite_storage, vault_online, vault_record # Initialize configuration and authentication context config = configuration.JsonConfigurationStorage() -keeper_endpoint = endpoint.KeeperEndpoint(config) +server = 'keepersecurity.com' # can be set to keepersecurity.com, keepersecurity.com.au, keepersecurity.jp, keepersecurity.eu, keepersecurity.ca, govcloud.keepersecurity.us +keeper_endpoint = endpoint.KeeperEndpoint(config, keeper_server=server) login_auth_context = login_auth.LoginAuth(keeper_endpoint) # Authenticate user @@ -158,11 +161,11 @@ while not login_auth_context.login_step.is_final(): if isinstance(login_auth_context.login_step, login_auth.LoginStepDeviceApproval): login_auth_context.login_step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - password = PromptSession().prompt('Enter password: ', is_password=True) + password = getpass.getpass('Enter password: ') login_auth_context.login_step.verify_password(password) elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): channel = login_auth_context.login_step.get_channels()[0] - code = PromptSession().prompt(f'Enter 2FA code for {channel.channel_name}: ', is_password=True) + code = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') login_auth_context.login_step.send_code(channel.channel_uid, code) else: raise NotImplementedError() @@ -201,6 +204,8 @@ if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): print(f'Record Type: {record.record_type}') print("-" * 50) + vault.close() + keeper_auth.close() ``` **Important Security Notes:** diff --git a/keepercli-package/src/keepercli/commands/enterprise_user.py b/keepercli-package/src/keepercli/commands/enterprise_user.py index d01b4b82..890b3e69 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_user.py +++ b/keepercli-package/src/keepercli/commands/enterprise_user.py @@ -1,14 +1,72 @@ import argparse import json -from typing import Dict, List, Optional, Any, Set - +import datetime +from keepersdk.enterprise.enterprise_types import DeviceApprovalRequest +import time +from typing import Dict, List, Optional, Any, Set, TypedDict + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from keepersdk import utils from keepersdk.enterprise import batch_management, enterprise_management -from keepersdk.proto import APIRequest_pb2 +from keepersdk.proto import APIRequest_pb2,enterprise_pb2 from . import base, enterprise_utils from .. import api, prompt_utils from ..helpers import report_utils from ..params import KeeperParams +from cryptography.hazmat.primitives.asymmetric import ec + + +logger = api.get_logger() + +# Constants for EnterpriseDeviceApprovalCommand +GET_ENTERPRISE_USER_DATA_KEY_ENDPOINT = 'enterprise/get_enterprise_user_data_key' +GET_USER_DATA_KEY_SHARED_TO_ENTERPRISE_ENDPOINT = 'enterprise/get_user_data_key_shared_to_enterprise' +APPROVE_USER_DEVICES_ENDPOINT = 'enterprise/approve_user_devices' + +AUDIT_REPORT_COMMAND = 'get_audit_event_reports' +AUDIT_REPORT_TYPE = 'span' +AUDIT_REPORT_SCOPE = 'enterprise' +AUDIT_REPORT_COLUMNS = ['ip_address', 'username'] +AUDIT_EVENT_TYPE_LOGIN = 'login' +AUDIT_EVENT_LIMIT = 1000 + +TRUSTED_IP_LOOKBACK_DAYS = 365 +TOKEN_PREFIX_LENGTH = 10 +ECC_PUBLIC_KEY_LENGTH = 65 +TIMESTAMP_MILLISECONDS_TO_SECONDS = 1000.0 + +DEVICE_REPORT_HEADERS = [ + 'Date', + 'Email', + 'Device ID', + 'Device Name', + 'Device Type', + 'IP Address', + 'Client Version', + 'Location' +] + + +class AuditEventCreatedFilter(TypedDict): + min: int + + +class AuditEventFilter(TypedDict): + audit_event_type: str + created: AuditEventCreatedFilter + username: List[str] + + +class AuditEventReportRequest(TypedDict): + command: str + report_type: str + scope: str + columns: List[str] + filter: AuditEventFilter + limit: int + class EnterpriseUserCommand(base.GroupCommand): def __init__(self): @@ -19,6 +77,7 @@ def __init__(self): self.register_command(EnterpriseUserDeleteCommand(), 'delete') self.register_command(EnterpriseUserActionCommand(), 'action') self.register_command(EnterpriseUserAliasCommand(), 'alias') + self.register_command(EnterpriseDeviceApprovalCommand(), 'device-approve') class EnterpriseUserViewCommand(base.ArgparseCommand): @@ -535,3 +594,394 @@ def execute(self, context: KeeperParams, **kwargs) -> None: context.enterprise_loader.load() +class EnterpriseDeviceApprovalCommand(base.ArgparseCommand, enterprise_management.IEnterpriseManagementLogger): + def __init__(self): + parser = argparse.ArgumentParser( + prog='device-approve', parents=[base.report_output_parser], + description='Approve Cloud SSO Devices.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + EnterpriseDeviceApprovalCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--reload', '-r', dest='reload', action='store_true', + help='reload list of pending approval requests') + parser.add_argument('--approve', '-a', dest='approve', action='store_true', + help='approve user devices') + parser.add_argument('--deny', '-d', dest='deny', action='store_true', help='deny user devices') + parser.add_argument('--trusted-ip', dest='check_ip', action='store_true', + help='approve only devices coming from a trusted IP address') + parser.add_argument('device', type=str, nargs='?', action="append", help='User email or device ID') + + def warning(self, message: str) -> None: + logger.warning(message) + + @staticmethod + def token_to_string(token: bytes) -> str: + """Convert device token bytes to hexadecimal string representation.""" + src = token[0:TOKEN_PREFIX_LENGTH] + if src.hex: + return src.hex() + return ''.join('{:02x}'.format(x) for x in src) + + def execute(self, context: KeeperParams, **kwargs) -> None: + """Main execution method for device approval command.""" + assert context.enterprise_data is not None + assert context.auth + assert context.enterprise_loader is not None + + if kwargs.get('reload'): + context.enterprise_loader.load() + + enterprise_data = context.enterprise_data + + approval_requests = self._load_approval_requests(enterprise_data) + if not approval_requests: + return + + if kwargs.get('approve') and kwargs.get('deny'): + raise base.CommandError('Cannot approve and deny devices at the same time') + + matching_devices = self._filter_matching_devices(approval_requests, enterprise_data, kwargs.get('device')) + if not matching_devices: + return + + if kwargs.get('approve') and kwargs.get('check_ip'): + matching_devices = self._filter_trusted_ip_devices(context, enterprise_data, matching_devices) + if not matching_devices: + return + + if kwargs.get('approve') or kwargs.get('deny'): + self._process_approval_denial(context, enterprise_data, matching_devices, kwargs) + else: + self._display_report(enterprise_data, matching_devices, kwargs) + + def _load_approval_requests(self, enterprise_data) -> List[DeviceApprovalRequest]: + """Load and return all pending device approval requests.""" + approval_requests: List[DeviceApprovalRequest] = list(enterprise_data.device_approval_requests.get_all_entities()) + if len(approval_requests) == 0: + logger.info('No pending approval requests') + return [] + return approval_requests + + def _filter_matching_devices(self, approval_requests: List[DeviceApprovalRequest], + enterprise_data, device_filters) -> Dict[str, DeviceApprovalRequest]: + """Filter devices based on device ID or user email filters.""" + matching_devices = {} + + for device in approval_requests: + device_id = device.encrypted_device_token + if not device_id: + continue + device_id = EnterpriseDeviceApprovalCommand.token_to_string(utils.base64_url_decode(device_id)) + + if self._device_matches_filter(device, device_id, enterprise_data, device_filters): + matching_devices[device_id] = device + + if len(matching_devices) == 0: + logger.info('No matching devices found') + return matching_devices + + def _device_matches_filter(self, device: DeviceApprovalRequest, device_id: str, + enterprise_data, device_filters) -> bool: + """Check if a device matches any of the provided filters.""" + if not isinstance(device_filters, (list, tuple)): + return True + + for name in device_filters: + if not name: + return True + if device_id.startswith(name): + return True + ent_user_id = device.enterprise_user_id + user = next((x for x in enterprise_data.users.get_all_entities() + if x.enterprise_user_id == ent_user_id), None) + if user and user.username == name: + return True + return False + + def _filter_trusted_ip_devices(self, context: KeeperParams, enterprise_data, + matching_devices: Dict[str, DeviceApprovalRequest]) -> Dict[str, DeviceApprovalRequest]: + """Filter devices to only include those from trusted IP addresses.""" + user_ids = set([x.enterprise_user_id for x in matching_devices.values()]) + emails = self._get_user_emails(enterprise_data, user_ids) + + ip_map = self._get_trusted_ip_map(context, list(emails.values())) + + trusted_devices = {} + for device_id, device in matching_devices.items(): + username = emails.get(device.enterprise_user_id) + ip_address = device.ip_address + is_trusted = username and ip_address and username in ip_map and ip_address in ip_map[username] + + if is_trusted: + trusted_devices[device_id] = device + else: + logger.warning("The user %s attempted to login from an unstrusted IP (%s). " + "To force the approval, run the same command without the --trusted-ip argument", + username, ip_address) + + if len(trusted_devices) == 0: + logger.info('No matching devices found') + return trusted_devices + + def _get_user_emails(self, enterprise_data, user_ids: Set[int]) -> Dict[int, str]: + """Build a mapping of user IDs to usernames.""" + emails = {} + for user in enterprise_data.users.get_all_entities(): + user_id = user.enterprise_user_id + if user_id in user_ids: + emails[user_id] = user.username + return emails + + def _get_trusted_ip_map(self, context: KeeperParams, emails: List[str]) -> Dict[str, Set[str]]: + """Get mapping of usernames to their trusted IP addresses from audit logs.""" + last_year = datetime.datetime.now() - datetime.timedelta(days=TRUSTED_IP_LOOKBACK_DAYS) + audit_request: AuditEventReportRequest = { + 'command': AUDIT_REPORT_COMMAND, + 'report_type': AUDIT_REPORT_TYPE, + 'scope': AUDIT_REPORT_SCOPE, + 'columns': AUDIT_REPORT_COLUMNS, + 'filter': { + 'audit_event_type': AUDIT_EVENT_TYPE_LOGIN, + 'created': { + 'min': int(last_year.timestamp()) + }, + 'username': emails + }, + 'limit': AUDIT_EVENT_LIMIT + } + + response = context.auth.execute_auth_command(audit_request) + ip_map = {} + + if response.get('audit_event_overview_report_rows'): + for row in response.get('audit_event_overview_report_rows'): + username = row.get('username') + if username: + if username not in ip_map: + ip_map[username] = set() + ip_map[username].add(row.get('ip_address')) + + return ip_map + + def _process_approval_denial(self, context: KeeperParams, enterprise_data, + matching_devices: Dict[str, DeviceApprovalRequest], kwargs: Dict[str, Any]) -> None: + """Process device approval or denial requests.""" + approve_rq = enterprise_pb2.ApproveUserDevicesRequest() + data_keys = {} + + if kwargs.get('approve'): + data_keys = self._collect_user_data_keys(context, enterprise_data, matching_devices) + + device_requests = self._build_device_requests(matching_devices, data_keys, kwargs) + if not device_requests: + return + + approve_rq.deviceRequests.extend(device_requests) + context.auth.execute_auth_rest(APPROVE_USER_DEVICES_ENDPOINT, approve_rq, + response_type=enterprise_pb2.ApproveUserDevicesResponse) + context.enterprise_loader.load() + + def _collect_user_data_keys(self, context: KeeperParams, enterprise_data, + matching_devices: Dict[str, DeviceApprovalRequest]) -> Dict[int, bytes]: + """Collect user data keys using ECC and RSA methods.""" + data_keys: Dict[int, bytes] = {} + user_ids = set([x.enterprise_user_id for x in matching_devices.values()]) + + # Try ECC method first + ecc_user_ids = user_ids.copy() + ecc_user_ids.difference_update(data_keys.keys()) + if ecc_user_ids: + ecc_keys = self._get_ecc_data_keys(context, enterprise_data, ecc_user_ids) + data_keys.update(ecc_keys) + + # Try RSA method for remaining users (Account Transfer) + rsa_user_ids = user_ids.copy() + rsa_user_ids.difference_update(data_keys.keys()) + if rsa_user_ids and not context.auth.auth_context.forbid_rsa: + rsa_keys = self._get_rsa_data_keys(context, enterprise_data, rsa_user_ids) + data_keys.update(rsa_keys) + + return data_keys + + def _get_ecc_data_keys(self, context: KeeperParams, enterprise_data, user_ids: Set[int]) -> Dict[int, bytes]: + """Get user data keys using ECC encryption.""" + data_keys: Dict[int, bytes] = {} + curve = ec.SECP256R1() + ecc_private_key = self._get_ecc_private_key(enterprise_data, curve) + + if not ecc_private_key: + return data_keys + + data_key_rq = APIRequest_pb2.UserDataKeyRequest() + data_key_rq.enterpriseUserId.extend(user_ids) + data_key_rs = context.auth.execute_auth_rest( + GET_ENTERPRISE_USER_DATA_KEY_ENDPOINT, data_key_rq, + response_type=APIRequest_pb2.EnterpriseUserDataKeys) + + for key in data_key_rs.keys: + enc_data_key = key.userEncryptedDataKey + if enc_data_key: + try: + ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point( + curve, enc_data_key[:ECC_PUBLIC_KEY_LENGTH]) + shared_key = ecc_private_key.exchange(ec.ECDH(), ephemeral_public_key) + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(shared_key) + enc_key = digest.finalize() + data_key = utils.crypto.decrypt_aes_v2(enc_data_key[ECC_PUBLIC_KEY_LENGTH:], enc_key) + data_keys[key.enterpriseUserId] = data_key + except Exception as e: + logger.debug(e) + + return data_keys + + def _get_ecc_private_key(self, enterprise_data, curve) -> Optional[Any]: + """Extract and decrypt the ECC private key from enterprise data.""" + if not enterprise_data.enterprise_info.keys: + return None + if not enterprise_data.enterprise_info.keys.ecc_encrypted_private_key: + return None + + try: + keys = enterprise_data.get_enterprise_data_keys() + ecc_private_key_data = utils.base64_url_decode(keys.ecc_encrypted_private_key) + ecc_private_key_data = utils.crypto.decrypt_aes_v2( + ecc_private_key_data, enterprise_data.enterprise_info.tree_key) + private_value = int.from_bytes(ecc_private_key_data, byteorder='big', signed=False) + return ec.derive_private_key(private_value, curve, default_backend()) + except Exception as e: + logger.debug(e) + return None + + def _get_rsa_data_keys(self, context: KeeperParams, enterprise_data, user_ids: Set[int]) -> Dict[int, bytes]: + """Get user data keys from Account Transfer using RSA encryption.""" + data_keys: Dict[int, bytes] = {} + data_key_rq = APIRequest_pb2.UserDataKeyRequest() + data_key_rq.enterpriseUserId.extend(user_ids) + data_key_rs = context.auth.execute_auth_rest( + GET_USER_DATA_KEY_SHARED_TO_ENTERPRISE_ENDPOINT, data_key_rq, + response_type=APIRequest_pb2.UserDataKeyResponse) + + if data_key_rs.noEncryptedDataKey: + user_ids_without_key = set(data_key_rs.noEncryptedDataKey) + usernames = [x.username for x in enterprise_data.users.get_all_entities() + if x.enterprise_user_id in user_ids_without_key] + if usernames: + logger.info('User(s) \"%s\" have no accepted account transfers or did not share encryption key', + ', '.join(usernames)) + + if data_key_rs.accessDenied: + denied_user_ids = set(data_key_rs.accessDenied) + usernames = [x.username for x in enterprise_data.users.get_all_entities() + if x.enterprise_user_id in denied_user_ids] + if usernames: + logger.info('You cannot manage these user(s): %s', ', '.join(usernames)) + + if data_key_rs.userDataKeys: + for dk in data_key_rs.userDataKeys: + try: + role_key = utils.crypto.decrypt_aes_v2(dk.roleKey, enterprise_data.enterprise_info.tree_key) + encrypted_private_key = utils.base64_url_decode(dk.privateKey) + decrypted_private_key = utils.crypto.decrypt_aes_v1(encrypted_private_key, role_key) + private_key = utils.crypto.load_rsa_private_key(decrypted_private_key) + + for user_dk in dk.enterpriseUserIdDataKeyPairs: + if user_dk.enterpriseUserId not in data_keys: + if user_dk.keyType in (enterprise_pb2.KT_NO_KEY, enterprise_pb2.KT_ENCRYPTED_BY_PUBLIC_KEY): + data_key = utils.crypto.decrypt_rsa(user_dk.encryptedDataKey, private_key) + data_keys[user_dk.enterpriseUserId] = data_key + except Exception as ex: + logger.debug(ex) + + return data_keys + + def _build_device_requests(self, matching_devices: Dict[str, DeviceApprovalRequest], + data_keys: Dict[int, bytes], kwargs: Dict[str, Any]) -> List[Any]: + """Build device approval/denial request messages.""" + device_requests = [] + curve = ec.SECP256R1() + is_denial = kwargs.get('deny') + is_approval = kwargs.get('approve') + + for device in matching_devices.values(): + device_rq = enterprise_pb2.ApproveUserDeviceRequest() + device_rq.enterpriseUserId = device.enterprise_user_id + device_rq.encryptedDeviceToken = utils.base64_url_decode(device.encrypted_device_token) + device_rq.denyApproval = is_denial + + if is_approval: + if not device.device_public_key or len(device.device_public_key) == 0: + continue + + data_key = data_keys.get(device.enterprise_user_id) + if not data_key: + continue + + encrypted_data_key = self._encrypt_device_data_key(device, data_key, curve) + if not encrypted_data_key: + continue + + device_rq.encryptedDeviceDataKey = encrypted_data_key + + device_requests.append(device_rq) + + return device_requests + + def _encrypt_device_data_key(self, device: DeviceApprovalRequest, data_key: bytes, curve) -> Optional[bytes]: + """Encrypt data key for device using ECDH key exchange.""" + try: + ephemeral_key = ec.generate_private_key(curve, default_backend()) + device_public_key = ec.EllipticCurvePublicKey.from_encoded_point( + curve, utils.base64_url_decode(device.device_public_key)) + shared_key = ephemeral_key.exchange(ec.ECDH(), device_public_key) + + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(shared_key) + enc_key = digest.finalize() + + encrypted_data_key = utils.crypto.encrypt_aes_v2(data_key, enc_key) + ephemeral_public_key = ephemeral_key.public_key().public_bytes( + serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint) + + return ephemeral_public_key + encrypted_data_key + except Exception as e: + logger.info(e) + return None + + def _display_report(self, enterprise_data, matching_devices: Dict[str, DeviceApprovalRequest], + kwargs: Dict[str, Any]) -> None: + """Display device approval request report.""" + logger.info('') + headers = DEVICE_REPORT_HEADERS.copy() + + if kwargs.get('format') == 'json': + headers = [x.replace(' ', '_').lower() for x in headers] + + rows = [] + for device_id, device in matching_devices.items(): + user = next((x for x in enterprise_data.users.get_all_entities() + if x.enterprise_user_id == device.enterprise_user_id), None) + if not user: + continue + + date_formatted = time.strftime('%Y-%m-%d %H:%M:%S', + time.gmtime(device.date / TIMESTAMP_MILLISECONDS_TO_SECONDS)) + + rows.append([ + date_formatted, + user.username, + device_id, + device.device_name, + device.device_type, + device.ip_address, + device.client_version, + device.location + ]) + + rows.sort(key=lambda x: x[0]) + return report_utils.dump_report_data(rows, headers, fmt=kwargs.get('format'), + filename=kwargs.get('output')) \ No newline at end of file diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 510cf021..fb7a5ec9 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -94,4 +94,5 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('audit-alert', audit_alert.AuditAlerts(), base.CommandScope.Enterprise) commands.register_command('audit-log', audit_log.AuditLogCommand(), base.CommandScope.Enterprise, 'al') commands.register_command('download-membership', importer_commands.DownloadMembershipCommand(), base.CommandScope.Enterprise) - commands.register_command('apply-membership', importer_commands.ApplyMembershipCommand(), base.CommandScope.Enterprise) \ No newline at end of file + commands.register_command('apply-membership', importer_commands.ApplyMembershipCommand(), base.CommandScope.Enterprise) + commands.register_command('device-approve', enterprise_user.EnterpriseDeviceApprovalCommand(), base.CommandScope.Enterprise) \ No newline at end of file diff --git a/keepersdk-package/src/keepersdk/enterprise/enterprise_data.py b/keepersdk-package/src/keepersdk/enterprise/enterprise_data.py index 8853e3d8..5ca0b86b 100644 --- a/keepersdk-package/src/keepersdk/enterprise/enterprise_data.py +++ b/keepersdk-package/src/keepersdk/enterprise/enterprise_data.py @@ -28,6 +28,7 @@ def __init__(self) -> None: self._scims = private_data.ScimEntity() self._sso_services = private_data.SsoServiceEntity() self._email_provision = private_data.EmailProvisionEntity() + self._device_approval_requests = private_data.DeviceApprovalRequestEntity() self._logger = utils.get_logger() self._entities: Dict[int, enterprise_types.IEnterpriseDataPlugin] = { @@ -50,6 +51,7 @@ def __init__(self) -> None: enterprise_pb2.SCIMS: self._scims, enterprise_pb2.SSO_SERVICES: self._sso_services, enterprise_pb2.EMAIL_PROVISION: self._email_provision, + enterprise_pb2.DEVICES_REQUEST_FOR_ADMIN_APPROVAL: self._device_approval_requests, } def get_plugin(self, entity_id: int) -> Optional[enterprise_types.IEnterpriseDataPlugin]: @@ -166,3 +168,7 @@ def sso_services(self): @property def email_provision(self): return self._email_provision + + @property + def device_approval_requests(self): + return self._device_approval_requests diff --git a/keepersdk-package/src/keepersdk/enterprise/enterprise_types.py b/keepersdk-package/src/keepersdk/enterprise/enterprise_types.py index c22b7989..8c74d7d7 100644 --- a/keepersdk-package/src/keepersdk/enterprise/enterprise_types.py +++ b/keepersdk-package/src/keepersdk/enterprise/enterprise_types.py @@ -63,6 +63,24 @@ class User: IUser: Type[User] = attrs.make_class('IUser', [], (User,), frozen=True) +@attrs.define(kw_only=True) +class DeviceApprovalRequest: + enterprise_user_id: int + device_id: int + encrypted_device_token: str + device_public_key: str + device_name: str + client_version: str + device_type: str + date: int + ip_address: str + location: str + email: str + account_uid: Optional[bytes] = None +# noinspection PyTypeChecker +IDeviceApprovalRequest: Type[DeviceApprovalRequest] = attrs.make_class('IDeviceApprovalRequest', [], (DeviceApprovalRequest,), frozen=True) + + @attrs.define(kw_only=True) class Team: team_uid: str @@ -522,6 +540,11 @@ def email_provision(self) -> IEntity[EmailProvision, int]: def managed_companies(self) -> IEntity[ManagedCompany, int]: pass + @property + @abc.abstractmethod + def device_approval_requests(self) -> IEntity[DeviceApprovalRequest, str]: + pass + @property @abc.abstractmethod def user_aliases(self) -> ILink[UserAlias, int, str]: diff --git a/keepersdk-package/src/keepersdk/enterprise/private_data.py b/keepersdk-package/src/keepersdk/enterprise/private_data.py index 23b5fa2a..4a62aebb 100644 --- a/keepersdk-package/src/keepersdk/enterprise/private_data.py +++ b/keepersdk-package/src/keepersdk/enterprise/private_data.py @@ -451,6 +451,35 @@ def convert_entity(self, data) -> enterprise_types.ManagedCompany: tree_key_role=proto_entity.tree_key_role, file_plan_type=proto_entity.filePlanType, add_ons=license_add_on) +class DeviceApprovalRequestEntity(_IEnterpriseEntity[enterprise_types.DeviceApprovalRequest, str]): + + def get_entity_key(self, entity: enterprise_types.DeviceApprovalRequest) -> str: + return f'{entity.enterprise_user_id}:{entity.device_id}' + + def convert_entity(self, data) -> enterprise_types.DeviceApprovalRequest: + proto_entity = enterprise_pb2.DeviceRequestForAdminApproval() + proto_entity.ParseFromString(data) + + return enterprise_types.DeviceApprovalRequest( + enterprise_user_id=proto_entity.enterpriseUserId, + device_id=proto_entity.deviceId, + encrypted_device_token=utils.base64_url_encode(proto_entity.encryptedDeviceToken), + device_public_key=utils.base64_url_encode(proto_entity.devicePublicKey), + device_name=proto_entity.deviceName, + client_version=proto_entity.clientVersion, + device_type=proto_entity.deviceType, + date=proto_entity.date, + ip_address=proto_entity.ipAddress, + location=proto_entity.location, + email=proto_entity.email, + account_uid=proto_entity.accountUid + ) + + @classmethod + def frozen_entity_type(cls) -> Type[enterprise_types.DeviceApprovalRequest]: + return enterprise_types.IDeviceApprovalRequest + + class QueuedTeamUserLink(enterprise_types.ILink[enterprise_types.QueuedTeamUser, str, int], enterprise_types.IEnterpriseDataPlugin): def __init__(self) -> None: super().__init__() From ded8b3f262c237aef6baa32facf9a62a39e780f9 Mon Sep 17 00:00:00 2001 From: Sergey Kolupaev Date: Sat, 1 Nov 2025 00:11:30 -0700 Subject: [PATCH 43/44] Github publish workflow --- ...sh-to-pypi.yml => publish-cli-to-pypi.yml} | 50 +++++----- .github/workflows/publish-sdk.yml | 94 ++++++++++++------- ...est-with-pytest.yml => run-unit-tests.yml} | 17 ++-- keepercli-package/requirements.txt | 2 +- keepercli-package/src/keepercli/__init__.py | 2 +- keepersdk-package/mypy.ini | 9 +- keepersdk-package/requirements.txt | 7 +- keepersdk-package/setup.cfg | 8 +- keepersdk-package/src/keepersdk/__init__.py | 2 +- .../keepersdk/authentication/keeper_auth.py | 45 ++------- .../keepersdk/authentication/login_auth.py | 12 ++- .../keepersdk/authentication/notifications.py | 77 +++------------ .../authentication/push_notifications.py | 94 +++++++++++++++++++ .../keepersdk/enterprise/account_transfer.py | 14 +-- .../enterprise/enterprise_user_management.py | 1 + keepersdk-package/src/keepersdk/vault/ksm.py | 8 +- .../src/keepersdk/vault/ksm_management.py | 6 +- .../src/keepersdk/vault/trash_management.py | 4 +- .../unit_tests/test_ksm_management.py | 2 +- keepersdk-package/unit_tests/test_login.py | 11 +-- keepersdk-package/unit_tests/test_utils.py | 4 +- 21 files changed, 251 insertions(+), 218 deletions(-) rename .github/workflows/{publish-to-pypi.yml => publish-cli-to-pypi.yml} (54%) rename .github/workflows/{test-with-pytest.yml => run-unit-tests.yml} (60%) create mode 100644 keepersdk-package/src/keepersdk/authentication/push_notifications.py diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-cli-to-pypi.yml similarity index 54% rename from .github/workflows/publish-to-pypi.yml rename to .github/workflows/publish-cli-to-pypi.yml index 6cd49350..6265f43a 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-cli-to-pypi.yml @@ -1,51 +1,52 @@ -name: Publish Commander to PyPi +name: Publish CLI to PyPi on: workflow_dispatch: inputs: version: - description: Version to release (Tag from Keeper-Security/keeper-sdk-pyton) + description: Version to release (Tag from Keeper-Security/keeper-sdk-python) required: true jobs: build-n-publish: - name: Build and publish Keeper SDK for Python 📦 to PyPI + name: Build and publish Keeper CLI for Python to TestPyPI runs-on: ubuntu-latest timeout-minutes: 25 # To keep builds from running too long - + permissions: + contents: read + steps: - name: Checkout source code uses: actions/checkout@v2 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' architecture: 'x64' - name: Build the package run: | python -m pip install -U setuptools pip build wheel twine - python -m build --wheel + python -m build --wheel keepercli-package - name: Archive the package uses: actions/upload-artifact@v3 with: - name: KeeperSdkWheel + name: KeeperCLIWheel retention-days: 1 - path: dist/* + path: keepercli-package/dist/* if-no-files-found: error - - name: Publish Commander to test PyPi + - name: Publish keepercli to test PyPi env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }} run: | - twine upload -r testpypi dist/* - + twine upload -r testpypi keepercli-package/dist/* publish-pypi: - name: Publish Keeper SDK to PyPi + name: Publish Keeper CLI to PyPi runs-on: ubuntu-latest needs: [build-n-publish] environment: prod @@ -53,27 +54,18 @@ jobs: steps: - uses: actions/download-artifact@v3 with: - name: CommanderWheel - path: dist + name: KeeperCLIWheel + path: keepercli-package/dist - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' - architecture: 'x64' - - - name: Retrieve secrets from Keeper - id: ksecrets - uses: Keeper-Security/ksm-action@master - with: - keeper-secret-config: ${{ secrets.KSM_COMMANDER_SECRET_CONFIG }} - secrets: | - gD5LOOhI5QbnSFk8mIg3gg/field/password > PYPI_PASSWORD + python-version: '3.11' - - name: Publish to PyPi + - name: Publish keepercli to PyPi env: TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ steps.ksecrets.outputs.PYPI_PASSWORD }} + TWINE_PASSWORD: ${{ secrets.PYPI_PUBLISH_TOKEN }} run: | python -m pip install -U setuptools pip wheel twine - twine upload dist/* + twine upload -r pypi keepercli-package/dist/* \ No newline at end of file diff --git a/.github/workflows/publish-sdk.yml b/.github/workflows/publish-sdk.yml index 88844220..3ecaaddd 100644 --- a/.github/workflows/publish-sdk.yml +++ b/.github/workflows/publish-sdk.yml @@ -1,72 +1,96 @@ -name: Publish Keeper SDK to PyPi - -on: [workflow_dispatch] +name: Publish Keeper SDK to PyPI +on: + workflow_dispatch: + inputs: + version: + description: Version to release (tag or branch) + required: true jobs: - build-wheel: - name: Build and publish Keeper SDK for Python 📦 to PyPI + build-and-test: + name: Build and test Keeper SDK package runs-on: ubuntu-latest - timeout-minutes: 25 # To keep builds from running too long + timeout-minutes: 25 + permissions: + contents: read steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.version }} - - name: Set up Python 3.11 - uses: actions/setup-python@v4 + - name: Set up Python 3.13 + uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.13' + + - name: Install dependencies + run: | + pip install keepersdk-package/ + + - name: Run unit tests + run: python -m unittest discover -s keepersdk-package/unit_tests/ - name: Build the package run: | - python3 -m pip install -U setuptools build wheel twine + python3 -m pip install -U build wheel twine python3 -m build --wheel keepersdk-package - name: Archive the package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: KeeperSdkWheel retention-days: 1 path: keepersdk-package/dist/* if-no-files-found: error - - name: Publish Commander to test PyPi + publish-test-pypi: + name: Publish to Test PyPI + runs-on: ubuntu-latest + needs: [build-and-test] + environment: test + + steps: + - uses: actions/download-artifact@v4 + with: + name: KeeperSdkWheel + path: keepersdk-package/dist + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Publish to Test PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }} run: | - twine upload -r testpypi dist/* - + python -m pip install -U twine + twine upload --repository testpypi keepersdk-package/dist/* publish-pypi: - name: Publish Keeper SDK to PyPi + name: Publish to Production PyPI runs-on: ubuntu-latest - needs: [build-wheel] + needs: [publish-test-pypi] environment: prod steps: - - uses: actions/download-artifact@v3 - with: - name: CommanderWheel - path: dist - - - name: Set up Python 3.10 - uses: actions/setup-python@v4 + - uses: actions/download-artifact@v4 with: - python-version: '3.11' + name: KeeperSdkWheel + path: keepersdk-package/dist - - name: Retrieve secrets from Keeper - id: ksecrets - uses: Keeper-Security/ksm-action@master + - name: Set up Python 3.13 + uses: actions/setup-python@v5 with: - keeper-secret-config: ${{ secrets.KSM_COMMANDER_SECRET_CONFIG }} - secrets: | - gD5LOOhI5QbnSFk8mIg3gg/field/password > PYPI_PASSWORD + python-version: '3.13' - - name: Publish to PyPi + - name: Publish to PyPI env: TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ steps.ksecrets.outputs.PYPI_PASSWORD }} + TWINE_PASSWORD: ${{ secrets.PYPI_PUBLISH_TOKEN }} run: | - python -m pip install -U setuptools pip wheel twine - twine upload dist/* + python -m pip install -U twine + twine upload keepersdk-package/dist/* \ No newline at end of file diff --git a/.github/workflows/test-with-pytest.yml b/.github/workflows/run-unit-tests.yml similarity index 60% rename from .github/workflows/test-with-pytest.yml rename to .github/workflows/run-unit-tests.yml index 447aeea3..1c6460c2 100644 --- a/.github/workflows/test-with-pytest.yml +++ b/.github/workflows/run-unit-tests.yml @@ -1,35 +1,36 @@ -name: Test with pytest +name: Test with unittest on: pull_request: branches: - - masterlet' + - master workflow_dispatch: env: PYTHONUNBUFFERED: 1 jobs: - test-with-pytest: + test-with-unittest: strategy: matrix: python-version: ['3.8', '3.14'] runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout branch uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install package with test dependencies + - name: Install package run: | - cd keepersdk-package - pip install .[test] + pip install -e keepersdk-package/ - name: Run unit tests - run: pytest keepersdk-package/unit_tests/ \ No newline at end of file + run: python -m unittest discover -s keepersdk-package/unit_tests/ \ No newline at end of file diff --git a/keepercli-package/requirements.txt b/keepercli-package/requirements.txt index 96bd2998..8ff4ffbc 100644 --- a/keepercli-package/requirements.txt +++ b/keepercli-package/requirements.txt @@ -8,4 +8,4 @@ cbor2; sys_platform == "darwin" and python_version>='3.10' pyobjc-framework-LocalAuthentication; sys_platform == "darwin" and python_version>='3.10' winrt-runtime; sys_platform == "win32" winrt-Windows.Foundation; sys_platform == "win32" -winrt-Windows.Security.Credentials.UI; sys_platform == "win32" +winrt-Windows.Security.Credentials.UI; sys_platform == "win32" \ No newline at end of file diff --git a/keepercli-package/src/keepercli/__init__.py b/keepercli-package/src/keepercli/__init__.py index b7c1ec0f..6bd77a3d 100644 --- a/keepercli-package/src/keepercli/__init__.py +++ b/keepercli-package/src/keepercli/__init__.py @@ -9,5 +9,5 @@ # Contact: commander@keepersecurity.com # -__version__ = '17.0.0' +__version__ = '1.0.0-beta01' diff --git a/keepersdk-package/mypy.ini b/keepersdk-package/mypy.ini index 2c5e80a2..4839e50a 100644 --- a/keepersdk-package/mypy.ini +++ b/keepersdk-package/mypy.ini @@ -1,7 +1,14 @@ [mypy] warn_no_return = False files = src/ -python_version = 3.8 +python_version = 3.9 [mypy-keepersdk.proto.*] ignore_errors = True + +[mypy-fido2.*] +follow_imports = skip +ignore_errors = True + +[mypy-keepersdk.authentication.yubikey] +ignore_errors = True diff --git a/keepersdk-package/requirements.txt b/keepersdk-package/requirements.txt index 1a8481d0..83f3555c 100644 --- a/keepersdk-package/requirements.txt +++ b/keepersdk-package/requirements.txt @@ -1,7 +1,6 @@ attrs>=23.1.0 -certifi -requests>=2.31.0 -cryptography>=41.0.7 +requests>=2.32.2 +cryptography>=45.0.1 protobuf>=5.28.3 -websockets>=12.0 +websockets>=13.1 fido2>=2.0.0; python_version>='3.10' diff --git a/keepersdk-package/setup.cfg b/keepersdk-package/setup.cfg index 4e06773c..f57db56c 100644 --- a/keepersdk-package/setup.cfg +++ b/keepersdk-package/setup.cfg @@ -26,10 +26,10 @@ package_dir = include_package_data = True install_requires = attrs>=23.1.0 - requests>=2.31.0 - cryptography>=40.0.0 - protobuf>=4.25.0 - websockets>=12.0 + requests>=2.32.2 + cryptography>=45.0.1 + protobuf>=5.28.3 + websockets>=13.1 fido2>=2.0.0; python_version>='3.10' diff --git a/keepersdk-package/src/keepersdk/__init__.py b/keepersdk-package/src/keepersdk/__init__.py index b3bbb4f3..b22d4696 100644 --- a/keepersdk-package/src/keepersdk/__init__.py +++ b/keepersdk-package/src/keepersdk/__init__.py @@ -10,6 +10,6 @@ # from . import background -__version__ = '0.9.10' +__version__ = '0.9.11' background.init() diff --git a/keepersdk-package/src/keepersdk/authentication/keeper_auth.py b/keepersdk-package/src/keepersdk/authentication/keeper_auth.py index 38091ef3..4382511d 100644 --- a/keepersdk-package/src/keepersdk/authentication/keeper_auth.py +++ b/keepersdk-package/src/keepersdk/authentication/keeper_auth.py @@ -1,7 +1,6 @@ from __future__ import annotations import abc -import asyncio import enum import json import logging @@ -14,7 +13,7 @@ from google.protobuf.json_format import MessageToJson from . import endpoint, notifications -from .. import errors, utils, crypto, background +from .. import errors, utils, crypto from ..proto import APIRequest_pb2 @@ -51,8 +50,8 @@ class UserKeys: class AuthContext: def __init__(self) -> None: self.username = '' - self.account_uid = b'' - self.session_token = b'' + self.account_uid: bytes = b'' + self.session_token: bytes = b'' self.session_token_restriction: SessionTokenRestriction = SessionTokenRestriction.Unrestricted self.data_key: bytes = b'' self.client_key: bytes = b'' @@ -70,7 +69,6 @@ def __init__(self) -> None: self.device_token: bytes = b'' self.device_private_key: Optional[EllipticCurvePrivateKey] = None self.forbid_rsa: bool = False - self.session_token: bytes = b'' self.message_session_uid: bytes = b'' @@ -100,13 +98,13 @@ def check_keepalive(self) -> bool: class KeeperAuth: - def __init__(self, keeper_endpoint: endpoint.KeeperEndpoint, auth_context: AuthContext) -> None: + def __init__(self, keeper_endpoint: endpoint.KeeperEndpoint, auth_context: AuthContext, + push_notifications: Optional[notifications.FanOut[Dict[str, Any]]] = None) -> None: self.keeper_endpoint = keeper_endpoint self.auth_context = auth_context - self._push_notifications = notifications.KeeperPushNotifications() + self._push_notifications: Optional[notifications.FanOut[Dict[str, Any]]] = push_notifications self._ttk: Optional[TimeToKeepalive] = None self._key_cache: Optional[Dict[str, UserKeys]] = None - self._use_pushes = False def __enter__(self): return self @@ -115,12 +113,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() @property - def push_notifications(self) -> notifications.FanOut[Dict[str, Any]]: + def push_notifications(self) -> Optional[notifications.FanOut[Dict[str, Any]]]: return self._push_notifications def close(self) -> None: - if self.push_notifications and not self.push_notifications.is_completed: - self.stop_pushes() + if self._push_notifications and not self._push_notifications.is_completed: + self._push_notifications.shutdown() def _update_ttk(self): if self._ttk: @@ -315,28 +313,3 @@ def get_user_keys(self, username: str) -> Optional[UserKeys]: def get_team_keys(self, team_uid: str) -> Optional[UserKeys]: if self._key_cache: return self._key_cache.get(team_uid) - - async def _push_server_guard(self): - transmission_key = utils.generate_aes_key() - self._use_pushes = True - try: - while self._use_pushes: - url = self.keeper_endpoint.get_push_url( - transmission_key, self.auth_context.device_token, self.auth_context.message_session_uid) - await self._push_notifications.main_loop(url, transmission_key, self.auth_context.session_token) - self.execute_auth_rest('keep_alive', None) - except Exception as e: - utils.get_logger().debug(e) - finally: - self._use_pushes = False - - @property - def use_pushes(self) -> bool: - return self._use_pushes - - def start_pushes(self): - asyncio.run_coroutine_threadsafe(self._push_server_guard(), loop=background.get_loop()) - - def stop_pushes(self): - self._use_pushes = False - self._push_notifications.shutdown() diff --git a/keepersdk-package/src/keepersdk/authentication/login_auth.py b/keepersdk-package/src/keepersdk/authentication/login_auth.py index f33905f0..eef25a5a 100644 --- a/keepersdk-package/src/keepersdk/authentication/login_auth.py +++ b/keepersdk-package/src/keepersdk/authentication/login_auth.py @@ -8,7 +8,7 @@ from cryptography.hazmat.primitives.asymmetric import ec from google.protobuf.json_format import MessageToDict -from . import endpoint, configuration, notifications, keeper_auth, auth_utils +from . import endpoint, configuration, keeper_auth, auth_utils, notifications, push_notifications from .. import crypto, utils, errors from ..proto import APIRequest_pb2, breachwatch_pb2, ssocloud_pb2 @@ -624,7 +624,7 @@ def _ensure_push_notifications(login: LoginAuth) -> None: if login.push_notifications: return - keeper_pushes = notifications.KeeperPushNotifications() + keeper_pushes = push_notifications.KeeperPushNotifications() transmission_key = utils.generate_aes_key() url = login.keeper_endpoint.get_push_url(transmission_key, login.context.device_token, login.context.message_session_uid) keeper_pushes.connect_to_push_channel(url, transmission_key) @@ -754,11 +754,15 @@ def _on_logged_in(login: LoginAuth, response: APIRequest_pb2.LoginResponse, auth_context.message_session_uid = login.context.message_session_uid keeper_endpoint = login.keeper_endpoint - logged_auth = keeper_auth.KeeperAuth(keeper_endpoint, auth_context) + # Create push_notifications if not provided (for testing or custom implementations) + push_notif = login.push_notifications if login.push_notifications is not None else push_notifications.KeeperPushNotifications() + logged_auth = keeper_auth.KeeperAuth(keeper_endpoint, auth_context, push_notifications=push_notif) _post_login(logged_auth) + # Start push notifications if unrestricted and using KeeperPushNotifications if auth_context.session_token_restriction == keeper_auth.SessionTokenRestriction.Unrestricted: - logged_auth.start_pushes() + if isinstance(push_notif, push_notifications.KeeperPushNotifications): + push_notif.start_push_server(logged_auth) login.login_step = _ConnectedLoginStep(logged_auth) logged_auth.on_idle() diff --git a/keepersdk-package/src/keepersdk/authentication/notifications.py b/keepersdk-package/src/keepersdk/authentication/notifications.py index 5f21322f..34cc6b2e 100644 --- a/keepersdk-package/src/keepersdk/authentication/notifications.py +++ b/keepersdk-package/src/keepersdk/authentication/notifications.py @@ -1,21 +1,13 @@ -import asyncio -import json -import ssl -from typing import Optional, TypeVar, Generic, Callable, List, Dict, Any +"""Generic observer/pub-sub pattern implementation.""" -import websockets -import websockets.frames -import websockets.protocol -import websockets.exceptions - -from .. import crypto, utils, background -from ..proto import push_pb2 -from . import endpoint +from typing import Optional, TypeVar, Generic, Callable, List M = TypeVar('M') class FanOut(Generic[M]): + """Generic fan-out/publish-subscribe pattern for distributing messages to multiple callbacks.""" + def __init__(self) -> None: self._callbacks: List[Callable[[M], Optional[bool]]] = [] self._is_completed = False @@ -25,6 +17,10 @@ def is_completed(self): return self._is_completed def push(self, message: M) -> None: + """Push a message to all registered callbacks. + + Callbacks that return True or raise exceptions are automatically removed. + """ if self._is_completed: return to_remove = [] @@ -38,11 +34,13 @@ def push(self, message: M) -> None: self._remove_indexes(to_remove) def register_callback(self, callback: Callable[[M], Optional[bool]]) -> None: + """Register a callback to receive pushed messages.""" if self._is_completed: return self._callbacks.append(callback) def remove_callback(self, callback: Callable[[M], Optional[bool]]) -> None: + """Remove a specific callback.""" if self._is_completed: return to_remove = [] @@ -52,6 +50,7 @@ def remove_callback(self, callback: Callable[[M], Optional[bool]]) -> None: self._remove_indexes(to_remove) def remove_all(self): + """Remove all registered callbacks.""" self._callbacks.clear() def _remove_indexes(self, to_remove: List[int]): @@ -61,58 +60,6 @@ def _remove_indexes(self, to_remove: List[int]): del self._callbacks[idx] def shutdown(self): + """Shutdown the FanOut, marking it as completed and removing all callbacks.""" self._is_completed = True self._callbacks.clear() - - -class KeeperPushNotifications(FanOut[Dict[str, Any]]): - def __init__(self) -> None: - super().__init__() - self._ws_app: Optional[websockets.ClientConnection] = None - - async def main_loop(self, push_url: str, transmission_key: bytes, data: Optional[bytes] = None): - logger = utils.get_logger() - try: - await self.close_ws() - except Exception as e: - logger.debug('Push notification close error: %s', e) - - ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) - if not endpoint.get_certificate_check(): - ssl_context.verify_mode = ssl.CERT_NONE - - try: - async with websockets.connect(push_url, ping_interval=30, open_timeout=4, ssl=ssl_context) as ws_app: - self._ws_app = ws_app - if data: - await ws_app.send(utils.base64_url_encode(data)) - async for message in ws_app: - if isinstance(message, bytes): - try: - decrypted_data = crypto.decrypt_aes_v2(message, transmission_key) - rs = push_pb2.WssClientResponse() - rs.ParseFromString(decrypted_data) - self.push(json.loads(rs.message)) - except Exception as e: - logger.debug('Push notification: decrypt error: ', e) - except Exception as e: - logger.debug('Push notification: exception: %s', e) - - logger.debug('Push notification: exit.') - if self._ws_app == ws_app: - self._ws_app = None - - async def close_ws(self): - ws_app = self._ws_app - if ws_app and ws_app.state == websockets.protocol.State.OPEN: - try: - await ws_app.close(websockets.frames.CloseCode.GOING_AWAY) - except Exception: - pass - - def connect_to_push_channel(self, push_url: str, transmission_key: bytes, data: Optional[bytes]=None) -> None: - asyncio.run_coroutine_threadsafe(self.main_loop(push_url, transmission_key, data), background.get_loop()) - - def shutdown(self): - super().shutdown() - asyncio.run_coroutine_threadsafe(self.close_ws(), loop=background.get_loop()).result() diff --git a/keepersdk-package/src/keepersdk/authentication/push_notifications.py b/keepersdk-package/src/keepersdk/authentication/push_notifications.py new file mode 100644 index 00000000..6c2d1f1c --- /dev/null +++ b/keepersdk-package/src/keepersdk/authentication/push_notifications.py @@ -0,0 +1,94 @@ +import asyncio +import json +import ssl +from typing import Optional, Dict, Any + +import websockets +import websockets.frames +import websockets.protocol +import websockets.exceptions + +from . import endpoint, notifications, keeper_auth +from .. import crypto, utils, background +from ..proto import push_pb2 + + +class KeeperPushNotifications(notifications.FanOut[Dict[str, Any]]): + """Keeper Security push notification handler with WebSocket connection management.""" + + def __init__(self) -> None: + super().__init__() + self._ws_app: Optional[websockets.ClientConnection] = None + self._use_pushes = False + + async def main_loop(self, push_url: str, transmission_key: bytes, data: Optional[bytes] = None): + """Main WebSocket connection loop for receiving push notifications.""" + logger = utils.get_logger() + try: + await self.close_ws() + except Exception as e: + logger.debug('Push notification close error: %s', e) + + ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + if not endpoint.get_certificate_check(): + ssl_context.verify_mode = ssl.CERT_NONE + + ws_app = None + try: + async with websockets.connect(push_url, ping_interval=30, open_timeout=4, ssl=ssl_context) as ws_app: + self._ws_app = ws_app + if data: + await ws_app.send(utils.base64_url_encode(data)) + async for message in ws_app: + if isinstance(message, bytes): + try: + decrypted_data = crypto.decrypt_aes_v2(message, transmission_key) + rs = push_pb2.WssClientResponse() + rs.ParseFromString(decrypted_data) + self.push(json.loads(rs.message)) + except Exception as e: + logger.debug('Push notification: decrypt error: ', e) + except Exception as e: + logger.debug('Push notification: exception: %s', e) + + logger.debug('Push notification: exit.') + if self._ws_app == ws_app: + self._ws_app = None + + async def close_ws(self): + """Close the WebSocket connection if open.""" + self._use_pushes = False + ws_app = self._ws_app + if ws_app and ws_app.state == websockets.protocol.State.OPEN: + try: + await ws_app.close(websockets.frames.CloseCode.GOING_AWAY) + except Exception: + pass + + def connect_to_push_channel(self, push_url: str, transmission_key: bytes, data: Optional[bytes] = None) -> None: + """Connect to a push notification channel.""" + asyncio.run_coroutine_threadsafe(self.main_loop(push_url, transmission_key, data), background.get_loop()) + + def shutdown(self): + """Shutdown push notifications and close connections.""" + super().shutdown() + asyncio.run_coroutine_threadsafe(self.close_ws(), loop=background.get_loop()).result() + + async def _push_server_guard(self, auth: keeper_auth.KeeperAuth): + """Guard loop that maintains push notification connection with keep-alive.""" + transmission_key = utils.generate_aes_key() + self._use_pushes = True + try: + while self._use_pushes: + url = auth.keeper_endpoint.get_push_url( + transmission_key, auth.auth_context.device_token, auth.auth_context.message_session_uid) + await self.main_loop(url, transmission_key, auth.auth_context.session_token) + auth.execute_auth_rest('keep_alive', None) + except Exception as e: + utils.get_logger().debug(e) + finally: + self._use_pushes = False + + def start_push_server(self, auth: keeper_auth.KeeperAuth): + """Start push notification server with authenticated session.""" + asyncio.run_coroutine_threadsafe(self._push_server_guard(auth), loop=background.get_loop()) diff --git a/keepersdk-package/src/keepersdk/enterprise/account_transfer.py b/keepersdk-package/src/keepersdk/enterprise/account_transfer.py index 39ffd807..89236dab 100644 --- a/keepersdk-package/src/keepersdk/enterprise/account_transfer.py +++ b/keepersdk-package/src/keepersdk/enterprise/account_transfer.py @@ -187,9 +187,8 @@ def _decrypt_transfer_key2(self, encrypted_key: bytes, key_type: int) -> bytes: raise AccountTransferError(f'Unsupported transfer key type: {key_type}') def _decrypt_legacy_transfer_key(self, pre_transfer: PreTransferResponse) -> bytes: - enterprise_data = self.loader.enterprise_data - tree_key = enterprise_data.enterprise_info.tree_key - + loader = self.loader + role_key = None if pre_transfer.role_key: rsa_private_key = self.auth.auth_context.rsa_private_key @@ -200,12 +199,9 @@ def _decrypt_legacy_transfer_key(self, pre_transfer: PreTransferResponse) -> byt rsa_private_key) elif pre_transfer.role_key_id: role_key_id = pre_transfer.role_key_id - role_keys2 = enterprise_data.role_keys.get_all_entities() - key_entry = next((x for x in role_keys2 if x.role_id == role_key_id), None) - if key_entry: - role_key = utils.base64_url_decode(key_entry.encrypted_key) - role_key = crypto.decrypt_aes_v2(role_key, tree_key) - + loader.load_role_keys([role_key_id]) + role_key = loader.get_role_keys(role_key_id) + if not role_key: raise AccountTransferError('Cannot decrypt role key') diff --git a/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py b/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py index 0d507e9e..f30b26ee 100644 --- a/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py +++ b/keepersdk-package/src/keepersdk/enterprise/enterprise_user_management.py @@ -351,6 +351,7 @@ def execute_provision_request( provision_request, response_type=enterprise_pb2.EnterpriseUsersProvisionResponse ) + assert rs is not None self._validate_provision_response(rs, email) return rs diff --git a/keepersdk-package/src/keepersdk/vault/ksm.py b/keepersdk-package/src/keepersdk/vault/ksm.py index be9a7633..41018f7f 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm.py +++ b/keepersdk-package/src/keepersdk/vault/ksm.py @@ -1,6 +1,6 @@ -import datetime +from datetime import datetime from dataclasses import dataclass -from typing import Optional +from typing import Optional, List @dataclass(frozen=True) @@ -31,5 +31,5 @@ class SecretsManagerApp: folders: int count: int last_access: datetime - client_devices: Optional[list[ClientDevice]] = None - shared_secrets: Optional[list[SharedSecretsInfo]] = None + client_devices: Optional[List[ClientDevice]] = None + shared_secrets: Optional[List[SharedSecretsInfo]] = None diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index f424b002..9cf40d63 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -1,6 +1,6 @@ import datetime import json -from typing import Optional +from typing import Optional, List, Union from . import vault_online, ksm, record_management, vault_types from ..proto.APIRequest_pb2 import GetApplicationsSummaryResponse, ApplicationShareType, GetAppInfoRequest, GetAppInfoResponse @@ -14,7 +14,7 @@ CLIENT_SHORT_ID_LENGTH = 8 -def list_secrets_manager_apps(vault: vault_online.VaultOnline) -> list[ksm.SecretsManagerApp]: +def list_secrets_manager_apps(vault: vault_online.VaultOnline) -> List[ksm.SecretsManagerApp]: response = vault.keeper_auth.execute_auth_rest( URL_GET_SUMMARY_API, request=None, @@ -139,7 +139,7 @@ def remove_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str return app.uid -def get_app_info(vault: vault_online.VaultOnline, app_uid: str | list[str]) -> list: +def get_app_info(vault: vault_online.VaultOnline, app_uid: Union[str, List[str]]) -> List: rq = GetAppInfoRequest() if isinstance(app_uid, str): diff --git a/keepersdk-package/src/keepersdk/vault/trash_management.py b/keepersdk-package/src/keepersdk/vault/trash_management.py index c41eff9b..61f8df5b 100644 --- a/keepersdk-package/src/keepersdk/vault/trash_management.py +++ b/keepersdk-package/src/keepersdk/vault/trash_management.py @@ -448,7 +448,7 @@ def get_trash_record(vault: vault_online.VaultOnline, record_uid: str) -> Tuple[ return record, is_shared -def restore_trash_records(vault: vault_online.VaultOnline, records: List[str], confirm: Optional[Callable[[str], bool]] = None) -> None: +def restore_trash_records(vault: vault_online.VaultOnline, records: List[str], confirm: Optional[Callable[[str], str]] = None) -> None: """Restore deleted records from trash. Args: @@ -658,7 +658,7 @@ def _is_restore_plan_empty(restore_plan: Dict[str, Any]) -> bool: return record_count == 0 and folder_count == 0 -def _confirm_restoration(restore_plan: Dict[str, Any], confirm_func: Callable[[str], bool]) -> bool: +def _confirm_restoration(restore_plan: Dict[str, Any], confirm_func: Callable[[str], str]) -> bool: """Confirm restoration with the user.""" record_count = len(restore_plan['records_to_restore']) for folder_records in restore_plan['folder_records_to_restore'].values(): diff --git a/keepersdk-package/unit_tests/test_ksm_management.py b/keepersdk-package/unit_tests/test_ksm_management.py index 69789595..7bc0d9c8 100644 --- a/keepersdk-package/unit_tests/test_ksm_management.py +++ b/keepersdk-package/unit_tests/test_ksm_management.py @@ -226,7 +226,7 @@ def test_create_app_duplicate_raises(self): self.vault.vault_data.records.return_value = [mock_record] with self.assertRaises(ValueError) as cm: ksm_management.create_secrets_manager_app(self.vault, 'TestApp') - self.assertEqual(str(cm.exception), 'Application with the same name TestApp already exists.') + self.assertEqual(str(cm.exception), 'Application with the same name TestApp already exists. Set force to true to add Application with same name') def test_create_app_duplicate_force_add(self): mock_record = MagicMock(title='TestApp') diff --git a/keepersdk-package/unit_tests/test_login.py b/keepersdk-package/unit_tests/test_login.py index cf9e95d1..f8050821 100644 --- a/keepersdk-package/unit_tests/test_login.py +++ b/keepersdk-package/unit_tests/test_login.py @@ -148,17 +148,10 @@ def execute_rest(rest_endpoint, request=None, response_type=None, session_token= session_token=session_token, payload_version=payload_version) - def connect_to_push_server(session_uid, device_token, data=None): - return notifications.FanOut() - mock = MagicMock() mock.side_effect = execute_rest keeper_endpoint.execute_rest = mock - mock = MagicMock() - mock.side_effect = connect_to_push_server - keeper_endpoint.connect_to_push_server = mock - mock = MagicMock() mock.side_effect = Exception keeper_endpoint.v2_execute = mock @@ -167,7 +160,9 @@ def connect_to_push_server(session_uid, device_token, data=None): mock.side_effect = Exception keeper_endpoint._communicate_keeper = mock - return login_auth.LoginAuth(keeper_endpoint) + auth = login_auth.LoginAuth(keeper_endpoint) + auth.push_notifications = notifications.FanOut() + return auth @staticmethod def reset_stops(): diff --git a/keepersdk-package/unit_tests/test_utils.py b/keepersdk-package/unit_tests/test_utils.py index 57b5e50f..a7ed5987 100644 --- a/keepersdk-package/unit_tests/test_utils.py +++ b/keepersdk-package/unit_tests/test_utils.py @@ -28,8 +28,8 @@ def test_dataclass(self): _ = ne.store_data(data, tree_key) ee: enterprise_types.IEnterpriseEntity[enterprise_types.Node, int] = ne for n in ee.get_all_entities(): - print(n) + pass # Test that iteration works def test_ore(self): a: Dict[Tuple[str, int], Any] = {('eeee', 33): 'rrrrr'} - print(a) + self.assertIsNotNone(a) From 30d9551a486dbb967ebf6f2b5c6927c22fe76ffc Mon Sep 17 00:00:00 2001 From: Sergey Kolupaev Date: Sat, 1 Nov 2025 00:17:17 -0700 Subject: [PATCH 44/44] Release 1.0.0 --- keepersdk-package/src/keepersdk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keepersdk-package/src/keepersdk/__init__.py b/keepersdk-package/src/keepersdk/__init__.py index b22d4696..d448bbd0 100644 --- a/keepersdk-package/src/keepersdk/__init__.py +++ b/keepersdk-package/src/keepersdk/__init__.py @@ -10,6 +10,6 @@ # from . import background -__version__ = '0.9.11' +__version__ = '1.0.0' background.init()