diff --git a/keepercommander/commands/discoveryrotation.py b/keepercommander/commands/discoveryrotation.py index 028693d2f..29a2797d7 100644 --- a/keepercommander/commands/discoveryrotation.py +++ b/keepercommander/commands/discoveryrotation.py @@ -2566,6 +2566,7 @@ class PamConfigurationEditMixin(RecordEditMixin): PAM_CONFIG_RECORD_TYPES = frozenset({ 'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration', 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration', + 'pamGitHubConfiguration', }) PAM_RESOURCE_RECORD_TYPES = frozenset({ 'pamDatabase', 'pamDirectory', 'pamMachine', 'pamRemoteBrowser', @@ -2758,7 +2759,7 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va extra_properties.append(f'text.pamGitHubId={github_id}') personal_access_token = kwargs.get('personal_access_token') if personal_access_token: - extra_properties.append(f'secret.personalAccessToken={personal_access_token}') + extra_properties.append(f'secret.pamGitHubPersonalAccessToken={personal_access_token}') github_base_url = kwargs.get('github_base_url') if github_base_url: extra_properties.append(f'text.pamGitHubBaseUrl={github_base_url}') @@ -2870,7 +2871,8 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va # Fields that the backend previously required but now treats as optional for pamAzureConfiguration. AZURE_OPTIONAL_FIELDS = frozenset({'clientId', 'clientSecret'}) - def verify_required(self, record): # type: (vault.TypedRecord) -> None + def verify_required(self, record, command=''): # type: (vault.TypedRecord, str) -> None + missing_fields = [] for field in record.fields: if field.required: if len(field.value) == 0: @@ -2882,10 +2884,15 @@ def verify_required(self, record): # type: (vault.TypedRecord) -> None and field.label in self.AZURE_OPTIONAL_FIELDS): pass else: - self.warnings.append(f'Empty required field: "{field.get_field_name()}"') + missing_fields.append(field.get_field_name()) for custom in record.custom: if custom.required: custom.required = False + if missing_fields: + if len(missing_fields) == 1: + raise CommandError(command, f'Empty required field: "{missing_fields[0]}"') + fields_text = ', '.join(f'"{x}"' for x in missing_fields) + raise CommandError(command, f'Empty required fields: {fields_text}') class PAMConfigurationNewCommand(Command, PamConfigurationEditMixin): @@ -2986,7 +2993,7 @@ def execute(self, params, **kwargs): if not gateway_uid: logging.warning(f'Gateway "{gw_name}" not found.') - self.verify_required(record) + self.verify_required(record, command='pam-config-new') create_pam_configuration_in_folder(params, record, shared_folder_uid, command='pam-config-new') @@ -3119,7 +3126,7 @@ def execute(self, params, **kwargs): orig_admin_cred_ref = value.get('adminCredentialRef') or '' self.parse_properties(params, configuration, config_edit=True, **kwargs) - self.verify_required(configuration) + self.verify_required(configuration, command='pam-config-edit') update_pam_record(params, configuration, command='pam-config-edit') diff --git a/keepercommander/commands/discoveryrotation_v1.py b/keepercommander/commands/discoveryrotation_v1.py index cadd5f0c9..8d2d21502 100644 --- a/keepercommander/commands/discoveryrotation_v1.py +++ b/keepercommander/commands/discoveryrotation_v1.py @@ -970,7 +970,8 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va if extra_properties: self.assign_typed_fields(record, [RecordEditMixin.parse_field(x) for x in extra_properties]) - def verify_required(self, record): # type: (vault.TypedRecord) -> None + def verify_required(self, record, command=''): # type: (vault.TypedRecord, str) -> None + missing_fields = [] for field in record.fields: if field.required: if len(field.value) == 0: @@ -981,10 +982,15 @@ def verify_required(self, record): # type: (vault.TypedRecord) -> None 'tz': 'Etc/UTC', }] else: - self.warnings.append(f'Empty required field: "{field.get_field_name()}"') + missing_fields.append(field.get_field_name()) for custom in record.custom: if custom.required: custom.required = False + if missing_fields: + if len(missing_fields) == 1: + raise CommandError(command, f'Empty required field: "{missing_fields[0]}"') + fields_text = ', '.join(f'"{x}"' for x in missing_fields) + raise CommandError(command, f'Empty required fields: {fields_text}') class PAMConfigurationNewCommand(Command, PamConfigurationEditMixin): @@ -1037,7 +1043,7 @@ def execute(self, params, **kwargs): if not shared_folder_uid: raise CommandError('pam-config-new', '--shared_folder parameter is required to create a PAM configuration') - self.verify_required(record) + self.verify_required(record, command='pam-config-new') pam_configuration_create_record_v6(params, record, shared_folder_uid) @@ -1126,7 +1132,7 @@ def execute(self, params, **kwargs): orig_shared_folder_uid = value.get('folderUid') or '' self.parse_properties(params, configuration, **kwargs) - self.verify_required(configuration) + self.verify_required(configuration, command='pam-config-edit') record_management.update_record(params, configuration) diff --git a/keepercommander/commands/universalsecretsync.py b/keepercommander/commands/universalsecretsync.py index 3dddfacfb..5bbda109e 100644 --- a/keepercommander/commands/universalsecretsync.py +++ b/keepercommander/commands/universalsecretsync.py @@ -78,7 +78,8 @@ def print_uss_configurations_list(params, format_type='table'): configurations = list(vault_extensions.find_records(params, record_version=6)) # Only process these specific configuration types - uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration') + uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration', + 'pamGitHubConfiguration') configs_data = [] for record in configurations: @@ -111,6 +112,9 @@ def print_uss_configurations_list(params, format_type='table'): # Get the configuration data config_data = universal_sync_edge.content_as_dict or {} + # GitHub-specific fields are nested under the 'github' key. + github_data = config_data.get('github') or {} + # Decrypt vault_name if present. The router stores it under the # 'vaultName' key as a base64-url string of the encrypted bytes. vault_name = 'N/A' @@ -143,6 +147,44 @@ def print_uss_configurations_list(params, format_type='table'): logging.debug(f"Failed to decrypt sync_identity for record {record.record_uid}: {e}") sync_identity = 'N/A' + # Decrypt owner if present. The router stores it under the + # 'github.owner' key as a base64-url string of the encrypted bytes. + owner = 'N/A' + owner_encrypted = github_data.get('owner') + if owner_encrypted: + try: + owner_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(owner_encrypted), record.record_key) + owner = owner_bytes.decode('utf-8') + except Exception as e: + logging.debug(f"Failed to decrypt owner for record {record.record_uid}: {e}") + owner = 'N/A' + + # scope/organizationVisibility are stored unencrypted (plain enum values) + scope_value = github_data.get('scope') + try: + scope_str = pam_pb2.GitHubScope.Name(scope_value) if scope_value is not None else 'N/A' + except ValueError: + scope_str = 'N/A' + + org_visibility_value = github_data.get('organizationVisibility') + try: + org_visibility_str = pam_pb2.GitHubOrganizationVisibility.Name(org_visibility_value) \ + if org_visibility_value is not None else 'N/A' + except ValueError: + org_visibility_str = 'N/A' + + # Decrypt GitHub repository names if present. The router stores them under + # the 'github.repos' key as a list of base64-url strings of the encrypted bytes. + repo_names = [] + for repo_encrypted in github_data.get('repos', []): + try: + repo_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(repo_encrypted), record.record_key) + repo_names.append(repo_bytes.decode('utf-8')) + except Exception as e: + logging.debug(f"Failed to decrypt repo name for record {record.record_uid}: {e}") + # Get folder UIDs - folders connected via universal_sync_folder (simple count only) folder_count = 0 for vertex in root.has_vertices(): @@ -160,6 +202,10 @@ def print_uss_configurations_list(params, format_type='table'): 'folder_count': folder_count, 'vault_name': vault_name, 'sync_identity': sync_identity, + 'scope': scope_str, + 'owner': owner, + 'organization_visibility': org_visibility_str, + 'repos': repo_names, }) except Exception as e: # Skip records that fail to load or don't have USS config @@ -180,13 +226,16 @@ def print_uss_configurations_list(params, format_type='table'): # Display as simple summary table table = [] - headers = ['Network UID', 'Title', 'Type', 'Enabled', 'Dry Run', 'Folders', 'Vault Name', 'Sync Identity'] + headers = ['Network UID', 'Title', 'Type', 'Enabled', 'Dry Run', 'Folders', 'Vault Name', 'Sync Identity', + 'Scope', 'Owner', 'Org Visibility', 'Repos'] for config in configs_data: enabled_str = f"{bcolors.OKGREEN}Yes{bcolors.ENDC}" if config['enabled'] else f"{bcolors.FAIL}No{bcolors.ENDC}" dry_run_str = f"{bcolors.WARNING}Yes{bcolors.ENDC}" if config['dry_run_enabled'] else "No" folder_count = config.get('folder_count', 0) folders_str = f"{folder_count} folder(s)" if folder_count > 0 else "None" + repos = config.get('repos', []) + repos_str = f"{len(repos)} repo(s)" if repos else "None" row = [ config['record_uid'], @@ -196,7 +245,11 @@ def print_uss_configurations_list(params, format_type='table'): dry_run_str, folders_str, config['vault_name'], - config['sync_identity'] + config['sync_identity'], + config.get('scope', 'N/A'), + config.get('owner', 'N/A'), + config.get('organization_visibility', 'N/A'), + repos_str ] table.append(row) @@ -219,7 +272,8 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): raise CommandError('', f'{bcolors.FAIL}Network "{network_uid}" not found{bcolors.ENDC}') # Check if it's a supported USS configuration type - uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration') + uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration', + 'pamGitHubConfiguration') if not isinstance(network, vault.TypedRecord) or network.record_type not in uss_supported_types: if format_type == 'json': return json.dumps({"error": f'Record "{network_uid}" is not a USS configuration'}) @@ -253,6 +307,9 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): # Get the configuration data config_data = universal_sync_edge.content_as_dict or {} + # GitHub-specific fields are nested under the 'github' key. + github_data = config_data.get('github') or {} + # Decrypt vault_name if present. The router stores it under the # 'vaultName' key as a base64-url string of the encrypted bytes. vault_name = 'N/A' @@ -285,6 +342,44 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): logging.debug(f"Failed to decrypt sync_identity for network {network.record_uid}: {e}") sync_identity = 'N/A' + # Decrypt owner if present. The router stores it under the + # 'github.owner' key as a base64-url string of the encrypted bytes. + owner = 'N/A' + owner_encrypted = github_data.get('owner') + if owner_encrypted: + try: + owner_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(owner_encrypted), network.record_key) + owner = owner_bytes.decode('utf-8') + except Exception as e: + logging.debug(f"Failed to decrypt owner for network {network.record_uid}: {e}") + owner = 'N/A' + + # scope/organizationVisibility are stored unencrypted (plain enum values) + scope_value = github_data.get('scope') + try: + scope_str = pam_pb2.GitHubScope.Name(scope_value) if scope_value is not None else 'N/A' + except ValueError: + scope_str = 'N/A' + + org_visibility_value = github_data.get('organizationVisibility') + try: + org_visibility_str = pam_pb2.GitHubOrganizationVisibility.Name(org_visibility_value) \ + if org_visibility_value is not None else 'N/A' + except ValueError: + org_visibility_str = 'N/A' + + # Decrypt GitHub repository names if present. The router stores them under + # the 'github.repos' key as a list of base64-url strings of the encrypted bytes. + repo_names = [] + for repo_encrypted in github_data.get('repos', []): + try: + repo_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(repo_encrypted), network.record_key) + repo_names.append(repo_bytes.decode('utf-8')) + except Exception as e: + logging.debug(f"Failed to decrypt repo name for network {network.record_uid}: {e}") + # Get folder vertices with sync status folder_vertices = [] for vertex in root.has_vertices(): @@ -339,6 +434,10 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): 'dry_run_enabled': config_data.get('dryRunEnabled', False), 'vault_name': vault_name, 'sync_identity': sync_identity, + 'scope': scope_str, + 'owner': owner, + 'organization_visibility': org_visibility_str, + 'repos': repo_names, 'folders': [] } @@ -370,6 +469,10 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): table.append(['Dry Run', 'Yes' if config_data.get('dryRunEnabled', False) else 'No']) table.append(['Vault Name', vault_name]) table.append(['Sync Identity', sync_identity]) + table.append(['Scope', scope_str]) + table.append(['Owner', owner]) + table.append(['Org Visibility', org_visibility_str]) + table.append(['Repos', ', '.join(repo_names) if repo_names else 'None']) table.append(['', '']) # Blank row separator # Display folder sync details @@ -430,6 +533,16 @@ class PAMUniversalSyncConfigAddCommand(Command): help='Identity record UID to use for syncing') parser.add_argument('--vault-name', '-vn', dest='vault_name', action='store', help='Vault name for universal sync') + parser.add_argument('--scope', '-sc', dest='scope', action='store', + choices=['repository', 'organization'], + help='GitHub sync scope: a single repository or an entire organization') + parser.add_argument('--owner', '-o', dest='owner', action='store', + help='GitHub organization or user name that owns the repositories') + parser.add_argument('--org-visibility', '-ov', dest='org_visibility', action='store', + choices=['all', 'private', 'selected'], + help='Repository visibility to sync when scope is organization') + parser.add_argument('--repo', '-r', dest='repo', action='append', + help='GitHub repository name to sync (can be specified multiple times; scope must be repository)') def get_parser(self): return PAMUniversalSyncConfigAddCommand.parser @@ -480,6 +593,29 @@ def execute(self, params, **kwargs): encrypted_vault_name = crypto.encrypt_aes_v2(vault_name_bytes, network.record_key) rq.vaultName = encrypted_vault_name + scope = kwargs.get('scope') + if scope is not None: + rq.github.scope = pam_pb2.ORGANIZATION if scope == 'organization' else pam_pb2.REPOSITORY + + owner = kwargs.get('owner') + if owner: + owner_bytes = string_to_bytes(owner) + encrypted_owner = crypto.encrypt_aes_v2(owner_bytes, network.record_key) + rq.github.owner = encrypted_owner + + org_visibility = kwargs.get('org_visibility') + if org_visibility is not None: + org_visibility_map = {'all': pam_pb2.ALL, 'private': pam_pb2.PRIVATE, 'selected': pam_pb2.SELECTED} + rq.github.organizationVisibility = org_visibility_map[org_visibility] + + repos = kwargs.get('repo') + if repos: + for repo_name in repos: + repo_bytes = string_to_bytes(repo_name) + repo_obj = pam_pb2.GitHubRepository() + repo_obj.name = crypto.encrypt_aes_v2(repo_bytes, network.record_key) + rq.github.repos.append(repo_obj) + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(params) try: @@ -505,6 +641,16 @@ class PAMUniversalSyncConfigEditCommand(Command): help='Identity record UID to use for syncing') parser.add_argument('--vault-name', '-vn', dest='vault_name', action='store', help='Vault name for universal sync') + parser.add_argument('--scope', '-sc', dest='scope', action='store', + choices=['repository', 'organization'], + help='GitHub sync scope: a single repository or an entire organization') + parser.add_argument('--owner', '-o', dest='owner', action='store', + help='GitHub organization or user name that owns the repositories') + parser.add_argument('--org-visibility', '-ov', dest='org_visibility', action='store', + choices=['all', 'private', 'selected'], + help='Repository visibility to sync when scope is organization') + parser.add_argument('--repo', '-r', dest='repo', action='append', + help='GitHub repository name to sync (can be specified multiple times; scope must be repository)') def get_parser(self): return PAMUniversalSyncConfigEditCommand.parser @@ -601,6 +747,45 @@ def execute(self, params, **kwargs): elif existing_config.get('vaultName'): rq.vaultName = utils.base64_url_decode(existing_config['vaultName']) + # GitHub-specific fields live under the nested 'github' object, both in the + # request message (rq.github) and in the existing DAG edge content. + existing_github = existing_config.get('github') or {} + + scope = kwargs.get('scope') + if scope is not None: + rq.github.scope = pam_pb2.ORGANIZATION if scope == 'organization' else pam_pb2.REPOSITORY + elif 'scope' in existing_github: + rq.github.scope = existing_github['scope'] + + owner = kwargs.get('owner') + if owner: + owner_bytes = string_to_bytes(owner) + encrypted_owner = crypto.encrypt_aes_v2(owner_bytes, network.record_key) + rq.github.owner = encrypted_owner + elif existing_github.get('owner'): + rq.github.owner = utils.base64_url_decode(existing_github['owner']) + + org_visibility = kwargs.get('org_visibility') + if org_visibility is not None: + org_visibility_map = {'all': pam_pb2.ALL, 'private': pam_pb2.PRIVATE, 'selected': pam_pb2.SELECTED} + rq.github.organizationVisibility = org_visibility_map[org_visibility] + elif 'organizationVisibility' in existing_github: + rq.github.organizationVisibility = existing_github['organizationVisibility'] + + # Handle repos - if provided, replace all; if not, keep existing (re-encrypted values carried forward as-is) + repos = kwargs.get('repo') + if repos: + for repo_name in repos: + repo_bytes = string_to_bytes(repo_name) + repo_obj = pam_pb2.GitHubRepository() + repo_obj.name = crypto.encrypt_aes_v2(repo_bytes, network.record_key) + rq.github.repos.append(repo_obj) + else: + for existing_repo in existing_github.get('repos', []): + repo_obj = pam_pb2.GitHubRepository() + repo_obj.name = utils.base64_url_decode(existing_repo) + rq.github.repos.append(repo_obj) + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(params) try: diff --git a/keepercommander/proto/pam_pb2.py b/keepercommander/proto/pam_pb2.py index 05f9d0a0c..b773f6c8e 100644 --- a/keepercommander/proto/pam_pb2.py +++ b/keepercommander/proto/pam_pb2.py @@ -26,7 +26,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\"P\n\x1dPAMSetMaxInstanceCountRequest\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x18\n\x10maxInstanceCount\x18\x02 \x01(\x05\"%\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\"\x81\x02\n\x14PAMRecordingsRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\x89\x02\n\x1cPAMRecordingsForUsersRequest\x12\x11\n\tusernames\x18\x01 \x03(\t\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\xd4\x02\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\x11\n\tcreatedOn\x18\x08 \x01(\x03\x12\x10\n\x08protocol\x18\t \x01(\t\x12\x13\n\x0b\x63loseReason\x18\n \x01(\x05\x12\x19\n\x11recordingDuration\x18\x0b \x01(\x05\x12\x36\n\x12\x61iOverallRiskLevel\x18\x0c \x01(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x18\n\x10\x61iOverallSummary\x18\r \x01(\x0c\"O\n\x15PAMRecordingsResponse\x12%\n\nrecordings\x18\x01 \x03(\x0b\x32\x11.PAM.PAMRecording\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\"*\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\"\x84\x03\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\x12\x1d\n\x10keeperAiSettings\x18\t \x01(\x0cH\x07\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_jitSettingsB\x13\n\x11_keeperAiSettings\"%\n\x16PAMUniversalSyncFolder\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\"\xfc\x01\n\x16PAMUniversalSyncConfig\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x14\n\x07\x65nabled\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1a\n\rdryRunEnabled\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12,\n\x07\x66olders\x18\x04 \x03(\x0b\x32\x1b.PAM.PAMUniversalSyncFolder\x12\x19\n\x0csyncIdentity\x18\x05 \x01(\x0cH\x02\x88\x01\x01\x12\x16\n\tvaultName\x18\x06 \x01(\x0cH\x03\x88\x01\x01\x42\n\n\x08_enabledB\x10\n\x0e_dryRunEnabledB\x0f\n\r_syncIdentityB\x0c\n\n_vaultName\"7\n\x11NhiMetricsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"\x9c\x02\n\x0ePamUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12!\n\x19recordRotationScheduledOk\x18\x02 \x01(\x05\x12\x1c\n\x14pamConnectionStarted\x18\x03 \x01(\x05\x12\x18\n\x10pamTunnelStarted\x18\x04 \x01(\x05\x12\x1b\n\x13\x64iscoveryJobStarted\x18\x05 \x01(\x05\x12 \n\x18recordRotationOnDemandOk\x18\x06 \x01(\x05\x12\"\n\x1apamSessionRecordingStarted\x18\x07 \x01(\x05\x12\x15\n\rpamRbiStarted\x18\x08 \x01(\x05\x12%\n\x1dpamSessionRbiRecordingStarted\x18\t \x01(\x05\"p\n\x0eNhiUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trotations\x18\x02 \x01(\x05\x12\x0f\n\x07tunnels\x18\x03 \x01(\x05\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x15\n\rdiscoveryJobs\x18\x05 \x01(\x05\"\x84\x02\n\x12NhiMetricsResponse\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x03\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x03 \x01(\x03\x12\x18\n\x10uniqueKsmDevices\x18\x04 \x01(\x05\x12\x18\n\x10pamGatewayOnline\x18\x05 \x01(\x05\x12/\n\x0epamUsageByUser\x18\x06 \x03(\x0b\x32\x13.PAM.PamUsageByUserB\x02\x18\x01\x12\x10\n\x08nhiCount\x18\x07 \x01(\x05\x12\x13\n\x0bksmNhiCount\x18\x08 \x01(\x05\x12(\n\x0busageByUser\x18\t \x03(\x0b\x32\x13.PAM.NhiUsageByUser\"D\n\x16NhiBulkMetricsResponse\x12*\n\tresponses\x18\x01 \x03(\x0b\x32\x17.PAM.NhiMetricsResponse\"N\n\x0bNhiUidEntry\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\"\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32\x10.PAM.NhiCategory\x12\x0e\n\x06ksmNhi\x18\x03 \x01(\x08\"7\n\x11GetNhiUidsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"4\n\x12GetNhiUidsResponse\x12\x1e\n\x04uids\x18\x01 \x03(\x0b\x32\x10.PAM.NhiUidEntry*\x9e\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\x12\x0e\n\nKUBERNETES\x10\n*@\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*\xc9\x01\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\x12\x19\n\x15\x43MT_ANALYZE_RECORDING\x10\x04\x12!\n\x1d\x43MT_WORKFLOW_ACCESS_ELEVATION\x10\x05\x12\x0b\n\x07\x43MT_USS\x10\x06\x12\x0c\n\x08\x43MT_INFO\x10\x07\x12\x12\n\x0e\x43MT_AUTOMATION\x10\x08*V\n\x10PAMRecordingType\x12\x0f\n\x0bPRT_SESSION\x10\x00\x12\x12\n\x0ePRT_TYPESCRIPT\x10\x01\x12\x0c\n\x08PRT_TIME\x10\x02\x12\x0f\n\x0bPRT_SUMMARY\x10\x03*i\n\x15PAMRecordingRiskLevel\x12\x13\n\x0fPRR_UNSPECIFIED\x10\x00\x12\x0b\n\x07PRR_LOW\x10\x01\x12\x0e\n\nPRR_MEDIUM\x10\x02\x12\x0c\n\x08PRR_HIGH\x10\x03\x12\x10\n\x0cPRR_CRITICAL\x10\x04*`\n\x0bNhiCategory\x12\x18\n\x14NHI_CATEGORY_UNKNOWN\x10\x00\x12\x0c\n\x08PAM_USER\x10\x01\x12\x10\n\x0cPAM_RESOURCE\x10\x02\x12\x0b\n\x07GATEWAY\x10\x03\x12\n\n\x06\x44\x45VICE\x10\x04\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\"\xab\x02\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\x12\x18\n\x10maxInstanceCount\x18\x0b \x01(\x05\x12\x10\n\x08lastSeen\x18\x0c \x01(\x03\"P\n\x1dPAMSetMaxInstanceCountRequest\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x18\n\x10maxInstanceCount\x18\x02 \x01(\x05\"%\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\"\\\n\x10KturnAccessCreds\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\x0c\x12\x18\n\x10\x63\x65rt_fingerprint\x18\x03 \x01(\t\x12\x12\n\nexpires_at\x18\x04 \x01(\x04\"\x81\x02\n\x14PAMRecordingsRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\x89\x02\n\x1cPAMRecordingsForUsersRequest\x12\x11\n\tusernames\x18\x01 \x03(\t\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\xd4\x02\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\x11\n\tcreatedOn\x18\x08 \x01(\x03\x12\x10\n\x08protocol\x18\t \x01(\t\x12\x13\n\x0b\x63loseReason\x18\n \x01(\x05\x12\x19\n\x11recordingDuration\x18\x0b \x01(\x05\x12\x36\n\x12\x61iOverallRiskLevel\x18\x0c \x01(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x18\n\x10\x61iOverallSummary\x18\r \x01(\x0c\"O\n\x15PAMRecordingsResponse\x12%\n\nrecordings\x18\x01 \x03(\x0b\x32\x11.PAM.PAMRecording\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\"*\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\"5\n\x0fPAMServiceNames\x12\x13\n\x0bresourceUid\x18\x01 \x01(\x0c\x12\r\n\x05names\x18\x02 \x01(\x0c\"\xb4\x03\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\x12\x1d\n\x10keeperAiSettings\x18\t \x01(\x0cH\x07\x88\x01\x01\x12\x1b\n\x0eupdateServices\x18\n \x01(\x08H\x08\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_jitSettingsB\x13\n\x11_keeperAiSettingsB\x11\n\x0f_updateServices\"%\n\x16PAMUniversalSyncFolder\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\" \n\x10GitHubRepository\x12\x0c\n\x04name\x18\x01 \x01(\x0c\"\xd6\x01\n\x0cGitHubConfig\x12$\n\x05scope\x18\x01 \x01(\x0e\x32\x10.PAM.GitHubScopeH\x00\x88\x01\x01\x12\r\n\x05owner\x18\x02 \x01(\x0c\x12\x46\n\x16organizationVisibility\x18\x03 \x01(\x0e\x32!.PAM.GitHubOrganizationVisibilityH\x01\x88\x01\x01\x12$\n\x05repos\x18\x04 \x03(\x0b\x32\x15.PAM.GitHubRepositoryB\x08\n\x06_scopeB\x19\n\x17_organizationVisibility\"\xaf\x02\n\x16PAMUniversalSyncConfig\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x14\n\x07\x65nabled\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1a\n\rdryRunEnabled\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12,\n\x07\x66olders\x18\x04 \x03(\x0b\x32\x1b.PAM.PAMUniversalSyncFolder\x12\x19\n\x0csyncIdentity\x18\x05 \x01(\x0cH\x02\x88\x01\x01\x12\x16\n\tvaultName\x18\x06 \x01(\x0cH\x03\x88\x01\x01\x12&\n\x06github\x18\x07 \x01(\x0b\x32\x11.PAM.GitHubConfigH\x04\x88\x01\x01\x42\n\n\x08_enabledB\x10\n\x0e_dryRunEnabledB\x0f\n\r_syncIdentityB\x0c\n\n_vaultNameB\t\n\x07_github\"7\n\x11NhiMetricsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"\x9c\x02\n\x0ePamUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12!\n\x19recordRotationScheduledOk\x18\x02 \x01(\x05\x12\x1c\n\x14pamConnectionStarted\x18\x03 \x01(\x05\x12\x18\n\x10pamTunnelStarted\x18\x04 \x01(\x05\x12\x1b\n\x13\x64iscoveryJobStarted\x18\x05 \x01(\x05\x12 \n\x18recordRotationOnDemandOk\x18\x06 \x01(\x05\x12\"\n\x1apamSessionRecordingStarted\x18\x07 \x01(\x05\x12\x15\n\rpamRbiStarted\x18\x08 \x01(\x05\x12%\n\x1dpamSessionRbiRecordingStarted\x18\t \x01(\x05\"p\n\x0eNhiUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trotations\x18\x02 \x01(\x05\x12\x0f\n\x07tunnels\x18\x03 \x01(\x05\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x15\n\rdiscoveryJobs\x18\x05 \x01(\x05\"\x84\x02\n\x12NhiMetricsResponse\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x03\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x03 \x01(\x03\x12\x18\n\x10uniqueKsmDevices\x18\x04 \x01(\x05\x12\x18\n\x10pamGatewayOnline\x18\x05 \x01(\x05\x12/\n\x0epamUsageByUser\x18\x06 \x03(\x0b\x32\x13.PAM.PamUsageByUserB\x02\x18\x01\x12\x10\n\x08nhiCount\x18\x07 \x01(\x05\x12\x13\n\x0bksmNhiCount\x18\x08 \x01(\x05\x12(\n\x0busageByUser\x18\t \x03(\x0b\x32\x13.PAM.NhiUsageByUser\"D\n\x16NhiBulkMetricsResponse\x12*\n\tresponses\x18\x01 \x03(\x0b\x32\x17.PAM.NhiMetricsResponse\"^\n\x0bNhiUidEntry\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\"\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32\x10.PAM.NhiCategory\x12\x0e\n\x06ksmNhi\x18\x03 \x01(\x08\x12\x0e\n\x06\x61ppUid\x18\x04 \x01(\t\"7\n\x11GetNhiUidsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"4\n\x12GetNhiUidsResponse\x12\x1e\n\x04uids\x18\x01 \x03(\x0b\x32\x10.PAM.NhiUidEntry\"6\n\x1dSetNhiKsmEffectiveDateRequest\x12\x15\n\reffectiveDate\x18\x01 \x01(\x03\"L\n\x1eGetNhiKsmEffectiveDateResponse\x12\x15\n\reffectiveDate\x18\x01 \x01(\x03\x12\x13\n\x0b\x64\x65\x66\x61ultDate\x18\x02 \x01(\x03\"I\n\x1fPAMUniversalSyncPreCheckRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x12\n\nfolderUids\x18\x02 \x03(\x0c\"C\n\x1ePAMUniversalSyncPreCheckResult\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x0e\n\x06isUsed\x18\x02 \x01(\x08\"X\n PAMUniversalSyncPreCheckResponse\x12\x34\n\x07results\x18\x01 \x03(\x0b\x32#.PAM.PAMUniversalSyncPreCheckResult*\x9e\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\x12\x0e\n\nKUBERNETES\x10\n*@\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*\xc9\x01\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\x12\x19\n\x15\x43MT_ANALYZE_RECORDING\x10\x04\x12!\n\x1d\x43MT_WORKFLOW_ACCESS_ELEVATION\x10\x05\x12\x0b\n\x07\x43MT_USS\x10\x06\x12\x0c\n\x08\x43MT_INFO\x10\x07\x12\x12\n\x0e\x43MT_AUTOMATION\x10\x08*V\n\x10PAMRecordingType\x12\x0f\n\x0bPRT_SESSION\x10\x00\x12\x12\n\x0ePRT_TYPESCRIPT\x10\x01\x12\x0c\n\x08PRT_TIME\x10\x02\x12\x0f\n\x0bPRT_SUMMARY\x10\x03*i\n\x15PAMRecordingRiskLevel\x12\x13\n\x0fPRR_UNSPECIFIED\x10\x00\x12\x0b\n\x07PRR_LOW\x10\x01\x12\x0e\n\nPRR_MEDIUM\x10\x02\x12\x0c\n\x08PRR_HIGH\x10\x03\x12\x10\n\x0cPRR_CRITICAL\x10\x04*/\n\x0bGitHubScope\x12\x0e\n\nREPOSITORY\x10\x00\x12\x10\n\x0cORGANIZATION\x10\x01*B\n\x1cGitHubOrganizationVisibility\x12\x07\n\x03\x41LL\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\x0c\n\x08SELECTED\x10\x02*`\n\x0bNhiCategory\x12\x18\n\x14NHI_CATEGORY_UNKNOWN\x10\x00\x12\x0c\n\x08PAM_USER\x10\x01\x12\x10\n\x0cPAM_RESOURCE\x10\x02\x12\x0b\n\x07GATEWAY\x10\x03\x12\n\n\x06\x44\x45VICE\x10\x04\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,20 +36,24 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\003PAM' _globals['_NHIMETRICSRESPONSE'].fields_by_name['pamUsageByUser']._loaded_options = None _globals['_NHIMETRICSRESPONSE'].fields_by_name['pamUsageByUser']._serialized_options = b'\030\001' - _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=5340 - _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=5498 - _globals['_PAMOPERATIONTYPE']._serialized_start=5500 - _globals['_PAMOPERATIONTYPE']._serialized_end=5564 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=5566 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=5678 - _globals['_CONTROLLERMESSAGETYPE']._serialized_start=5681 - _globals['_CONTROLLERMESSAGETYPE']._serialized_end=5882 - _globals['_PAMRECORDINGTYPE']._serialized_start=5884 - _globals['_PAMRECORDINGTYPE']._serialized_end=5970 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=5972 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=6077 - _globals['_NHICATEGORY']._serialized_start=6079 - _globals['_NHICATEGORY']._serialized_end=6175 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=6267 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=6425 + _globals['_PAMOPERATIONTYPE']._serialized_start=6427 + _globals['_PAMOPERATIONTYPE']._serialized_end=6491 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=6493 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=6605 + _globals['_CONTROLLERMESSAGETYPE']._serialized_start=6608 + _globals['_CONTROLLERMESSAGETYPE']._serialized_end=6809 + _globals['_PAMRECORDINGTYPE']._serialized_start=6811 + _globals['_PAMRECORDINGTYPE']._serialized_end=6897 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=6899 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=7004 + _globals['_GITHUBSCOPE']._serialized_start=7006 + _globals['_GITHUBSCOPE']._serialized_end=7053 + _globals['_GITHUBORGANIZATIONVISIBILITY']._serialized_start=7055 + _globals['_GITHUBORGANIZATIONVISIBILITY']._serialized_end=7121 + _globals['_NHICATEGORY']._serialized_start=7123 + _globals['_NHICATEGORY']._serialized_end=7219 _globals['_PAMROTATIONSCHEDULE']._serialized_start=51 _globals['_PAMROTATIONSCHEDULE']._serialized_end=182 _globals['_PAMROTATIONSCHEDULESRESPONSE']._serialized_start=184 @@ -91,49 +95,67 @@ _globals['_PAMCONFIGURATIONS']._serialized_start=1883 _globals['_PAMCONFIGURATIONS']._serialized_end=1949 _globals['_PAMCONTROLLER']._serialized_start=1952 - _globals['_PAMCONTROLLER']._serialized_end=2207 - _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_start=2209 - _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_end=2289 - _globals['_CONTROLLERRESPONSE']._serialized_start=2291 - _globals['_CONTROLLERRESPONSE']._serialized_end=2328 - _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_start=2330 - _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_end=2407 - _globals['_CONFIGURATIONADDREQUEST']._serialized_start=2410 - _globals['_CONFIGURATIONADDREQUEST']._serialized_end=2573 - _globals['_RELAYACCESSCREDS']._serialized_start=2575 - _globals['_RELAYACCESSCREDS']._serialized_end=2649 - _globals['_PAMRECORDINGSREQUEST']._serialized_start=2652 - _globals['_PAMRECORDINGSREQUEST']._serialized_end=2909 - _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_start=2912 - _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_end=3177 - _globals['_PAMRECORDING']._serialized_start=3180 - _globals['_PAMRECORDING']._serialized_end=3520 - _globals['_PAMRECORDINGSRESPONSE']._serialized_start=3522 - _globals['_PAMRECORDINGSRESPONSE']._serialized_end=3601 - _globals['_PAMDATA']._serialized_start=3603 - _globals['_PAMDATA']._serialized_end=3645 - _globals['_UIDLIST']._serialized_start=3647 - _globals['_UIDLIST']._serialized_end=3670 - _globals['_PAMRESOURCECONFIG']._serialized_start=3673 - _globals['_PAMRESOURCECONFIG']._serialized_end=4061 - _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_start=4063 - _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_end=4100 - _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_start=4103 - _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_end=4355 - _globals['_NHIMETRICSREQUEST']._serialized_start=4357 - _globals['_NHIMETRICSREQUEST']._serialized_end=4412 - _globals['_PAMUSAGEBYUSER']._serialized_start=4415 - _globals['_PAMUSAGEBYUSER']._serialized_end=4699 - _globals['_NHIUSAGEBYUSER']._serialized_start=4701 - _globals['_NHIUSAGEBYUSER']._serialized_end=4813 - _globals['_NHIMETRICSRESPONSE']._serialized_start=4816 - _globals['_NHIMETRICSRESPONSE']._serialized_end=5076 - _globals['_NHIBULKMETRICSRESPONSE']._serialized_start=5078 - _globals['_NHIBULKMETRICSRESPONSE']._serialized_end=5146 - _globals['_NHIUIDENTRY']._serialized_start=5148 - _globals['_NHIUIDENTRY']._serialized_end=5226 - _globals['_GETNHIUIDSREQUEST']._serialized_start=5228 - _globals['_GETNHIUIDSREQUEST']._serialized_end=5283 - _globals['_GETNHIUIDSRESPONSE']._serialized_start=5285 - _globals['_GETNHIUIDSRESPONSE']._serialized_end=5337 + _globals['_PAMCONTROLLER']._serialized_end=2251 + _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_start=2253 + _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_end=2333 + _globals['_CONTROLLERRESPONSE']._serialized_start=2335 + _globals['_CONTROLLERRESPONSE']._serialized_end=2372 + _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_start=2374 + _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_end=2451 + _globals['_CONFIGURATIONADDREQUEST']._serialized_start=2454 + _globals['_CONFIGURATIONADDREQUEST']._serialized_end=2617 + _globals['_RELAYACCESSCREDS']._serialized_start=2619 + _globals['_RELAYACCESSCREDS']._serialized_end=2693 + _globals['_KTURNACCESSCREDS']._serialized_start=2695 + _globals['_KTURNACCESSCREDS']._serialized_end=2787 + _globals['_PAMRECORDINGSREQUEST']._serialized_start=2790 + _globals['_PAMRECORDINGSREQUEST']._serialized_end=3047 + _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_start=3050 + _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_end=3315 + _globals['_PAMRECORDING']._serialized_start=3318 + _globals['_PAMRECORDING']._serialized_end=3658 + _globals['_PAMRECORDINGSRESPONSE']._serialized_start=3660 + _globals['_PAMRECORDINGSRESPONSE']._serialized_end=3739 + _globals['_PAMDATA']._serialized_start=3741 + _globals['_PAMDATA']._serialized_end=3783 + _globals['_UIDLIST']._serialized_start=3785 + _globals['_UIDLIST']._serialized_end=3808 + _globals['_PAMSERVICENAMES']._serialized_start=3810 + _globals['_PAMSERVICENAMES']._serialized_end=3863 + _globals['_PAMRESOURCECONFIG']._serialized_start=3866 + _globals['_PAMRESOURCECONFIG']._serialized_end=4302 + _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_start=4304 + _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_end=4341 + _globals['_GITHUBREPOSITORY']._serialized_start=4343 + _globals['_GITHUBREPOSITORY']._serialized_end=4375 + _globals['_GITHUBCONFIG']._serialized_start=4378 + _globals['_GITHUBCONFIG']._serialized_end=4592 + _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_start=4595 + _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_end=4898 + _globals['_NHIMETRICSREQUEST']._serialized_start=4900 + _globals['_NHIMETRICSREQUEST']._serialized_end=4955 + _globals['_PAMUSAGEBYUSER']._serialized_start=4958 + _globals['_PAMUSAGEBYUSER']._serialized_end=5242 + _globals['_NHIUSAGEBYUSER']._serialized_start=5244 + _globals['_NHIUSAGEBYUSER']._serialized_end=5356 + _globals['_NHIMETRICSRESPONSE']._serialized_start=5359 + _globals['_NHIMETRICSRESPONSE']._serialized_end=5619 + _globals['_NHIBULKMETRICSRESPONSE']._serialized_start=5621 + _globals['_NHIBULKMETRICSRESPONSE']._serialized_end=5689 + _globals['_NHIUIDENTRY']._serialized_start=5691 + _globals['_NHIUIDENTRY']._serialized_end=5785 + _globals['_GETNHIUIDSREQUEST']._serialized_start=5787 + _globals['_GETNHIUIDSREQUEST']._serialized_end=5842 + _globals['_GETNHIUIDSRESPONSE']._serialized_start=5844 + _globals['_GETNHIUIDSRESPONSE']._serialized_end=5896 + _globals['_SETNHIKSMEFFECTIVEDATEREQUEST']._serialized_start=5898 + _globals['_SETNHIKSMEFFECTIVEDATEREQUEST']._serialized_end=5952 + _globals['_GETNHIKSMEFFECTIVEDATERESPONSE']._serialized_start=5954 + _globals['_GETNHIKSMEFFECTIVEDATERESPONSE']._serialized_end=6030 + _globals['_PAMUNIVERSALSYNCPRECHECKREQUEST']._serialized_start=6032 + _globals['_PAMUNIVERSALSYNCPRECHECKREQUEST']._serialized_end=6105 + _globals['_PAMUNIVERSALSYNCPRECHECKRESULT']._serialized_start=6107 + _globals['_PAMUNIVERSALSYNCPRECHECKRESULT']._serialized_end=6174 + _globals['_PAMUNIVERSALSYNCPRECHECKRESPONSE']._serialized_start=6176 + _globals['_PAMUNIVERSALSYNCPRECHECKRESPONSE']._serialized_end=6264 # @@protoc_insertion_point(module_scope) diff --git a/keepercommander/proto/pam_pb2.pyi b/keepercommander/proto/pam_pb2.pyi index b5a56975a..b83fdc735 100644 --- a/keepercommander/proto/pam_pb2.pyi +++ b/keepercommander/proto/pam_pb2.pyi @@ -4,7 +4,8 @@ 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 from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -63,6 +64,17 @@ class PAMRecordingRiskLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): PRR_HIGH: _ClassVar[PAMRecordingRiskLevel] PRR_CRITICAL: _ClassVar[PAMRecordingRiskLevel] +class GitHubScope(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + REPOSITORY: _ClassVar[GitHubScope] + ORGANIZATION: _ClassVar[GitHubScope] + +class GitHubOrganizationVisibility(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ALL: _ClassVar[GitHubOrganizationVisibility] + PRIVATE: _ClassVar[GitHubOrganizationVisibility] + SELECTED: _ClassVar[GitHubOrganizationVisibility] + class NhiCategory(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () NHI_CATEGORY_UNKNOWN: _ClassVar[NhiCategory] @@ -107,6 +119,11 @@ PRR_LOW: PAMRecordingRiskLevel PRR_MEDIUM: PAMRecordingRiskLevel PRR_HIGH: PAMRecordingRiskLevel PRR_CRITICAL: PAMRecordingRiskLevel +REPOSITORY: GitHubScope +ORGANIZATION: GitHubScope +ALL: GitHubOrganizationVisibility +PRIVATE: GitHubOrganizationVisibility +SELECTED: GitHubOrganizationVisibility NHI_CATEGORY_UNKNOWN: NhiCategory PAM_USER: NhiCategory PAM_RESOURCE: NhiCategory @@ -125,7 +142,7 @@ class PAMRotationSchedule(_message.Message): controllerUid: bytes scheduleData: str noSchedule: bool - def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., scheduleData: _Optional[str] = ..., noSchedule: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., scheduleData: _Optional[str] = ..., noSchedule: _Optional[bool] = ...) -> None: ... class PAMRotationSchedulesResponse(_message.Message): __slots__ = ("schedules",) @@ -304,7 +321,7 @@ class PAMConfigurations(_message.Message): def __init__(self, configurations: _Optional[_Iterable[_Union[PAMConfiguration, _Mapping]]] = ...) -> None: ... class PAMController(_message.Message): - __slots__ = ("controllerUid", "controllerName", "deviceToken", "deviceName", "nodeId", "created", "lastModified", "applicationUid", "appClientType", "isInitialized") + __slots__ = ("controllerUid", "controllerName", "deviceToken", "deviceName", "nodeId", "created", "lastModified", "applicationUid", "appClientType", "isInitialized", "maxInstanceCount", "lastSeen") CONTROLLERUID_FIELD_NUMBER: _ClassVar[int] CONTROLLERNAME_FIELD_NUMBER: _ClassVar[int] DEVICETOKEN_FIELD_NUMBER: _ClassVar[int] @@ -315,6 +332,8 @@ class PAMController(_message.Message): APPLICATIONUID_FIELD_NUMBER: _ClassVar[int] APPCLIENTTYPE_FIELD_NUMBER: _ClassVar[int] ISINITIALIZED_FIELD_NUMBER: _ClassVar[int] + MAXINSTANCECOUNT_FIELD_NUMBER: _ClassVar[int] + LASTSEEN_FIELD_NUMBER: _ClassVar[int] controllerUid: bytes controllerName: str deviceToken: str @@ -325,7 +344,9 @@ class PAMController(_message.Message): applicationUid: bytes appClientType: _enterprise_pb2.AppClientType isInitialized: bool - def __init__(self, controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., deviceToken: _Optional[str] = ..., deviceName: _Optional[str] = ..., nodeId: _Optional[int] = ..., created: _Optional[int] = ..., lastModified: _Optional[int] = ..., applicationUid: _Optional[bytes] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., isInitialized: bool = ...) -> None: ... + maxInstanceCount: int + lastSeen: int + def __init__(self, controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., deviceToken: _Optional[str] = ..., deviceName: _Optional[str] = ..., nodeId: _Optional[int] = ..., created: _Optional[int] = ..., lastModified: _Optional[int] = ..., applicationUid: _Optional[bytes] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., isInitialized: _Optional[bool] = ..., maxInstanceCount: _Optional[int] = ..., lastSeen: _Optional[int] = ...) -> None: ... class PAMSetMaxInstanceCountRequest(_message.Message): __slots__ = ("controllerUid", "maxInstanceCount") @@ -373,6 +394,18 @@ class RelayAccessCreds(_message.Message): serverTime: int def __init__(self, username: _Optional[str] = ..., password: _Optional[str] = ..., serverTime: _Optional[int] = ...) -> None: ... +class KturnAccessCreds(_message.Message): + __slots__ = ("url", "token", "cert_fingerprint", "expires_at") + URL_FIELD_NUMBER: _ClassVar[int] + TOKEN_FIELD_NUMBER: _ClassVar[int] + CERT_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + url: str + token: bytes + cert_fingerprint: str + expires_at: int + def __init__(self, url: _Optional[str] = ..., token: _Optional[bytes] = ..., cert_fingerprint: _Optional[str] = ..., expires_at: _Optional[int] = ...) -> None: ... + class PAMRecordingsRequest(_message.Message): __slots__ = ("recordUid", "maxCount", "rangeStart", "rangeEnd", "types", "risks", "protocols", "closeReasons") RECORDUID_FIELD_NUMBER: _ClassVar[int] @@ -393,6 +426,26 @@ class PAMRecordingsRequest(_message.Message): closeReasons: _containers.RepeatedScalarFieldContainer[int] def __init__(self, recordUid: _Optional[bytes] = ..., maxCount: _Optional[int] = ..., rangeStart: _Optional[int] = ..., rangeEnd: _Optional[int] = ..., types: _Optional[_Iterable[_Union[PAMRecordingType, str]]] = ..., risks: _Optional[_Iterable[_Union[PAMRecordingRiskLevel, str]]] = ..., protocols: _Optional[_Iterable[str]] = ..., closeReasons: _Optional[_Iterable[int]] = ...) -> None: ... +class PAMRecordingsForUsersRequest(_message.Message): + __slots__ = ("usernames", "maxCount", "rangeStart", "rangeEnd", "types", "risks", "protocols", "closeReasons") + USERNAMES_FIELD_NUMBER: _ClassVar[int] + MAXCOUNT_FIELD_NUMBER: _ClassVar[int] + RANGESTART_FIELD_NUMBER: _ClassVar[int] + RANGEEND_FIELD_NUMBER: _ClassVar[int] + TYPES_FIELD_NUMBER: _ClassVar[int] + RISKS_FIELD_NUMBER: _ClassVar[int] + PROTOCOLS_FIELD_NUMBER: _ClassVar[int] + CLOSEREASONS_FIELD_NUMBER: _ClassVar[int] + usernames: _containers.RepeatedScalarFieldContainer[str] + maxCount: int + rangeStart: int + rangeEnd: int + types: _containers.RepeatedScalarFieldContainer[PAMRecordingType] + risks: _containers.RepeatedScalarFieldContainer[PAMRecordingRiskLevel] + protocols: _containers.RepeatedScalarFieldContainer[str] + closeReasons: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, usernames: _Optional[_Iterable[str]] = ..., maxCount: _Optional[int] = ..., rangeStart: _Optional[int] = ..., rangeEnd: _Optional[int] = ..., types: _Optional[_Iterable[_Union[PAMRecordingType, str]]] = ..., risks: _Optional[_Iterable[_Union[PAMRecordingRiskLevel, str]]] = ..., protocols: _Optional[_Iterable[str]] = ..., closeReasons: _Optional[_Iterable[int]] = ...) -> None: ... + class PAMRecording(_message.Message): __slots__ = ("connectionUid", "recordingType", "recordUid", "userName", "startedOn", "length", "fileSize", "createdOn", "protocol", "closeReason", "recordingDuration", "aiOverallRiskLevel", "aiOverallSummary") CONNECTIONUID_FIELD_NUMBER: _ClassVar[int] @@ -429,7 +482,7 @@ class PAMRecordingsResponse(_message.Message): HASMORE_FIELD_NUMBER: _ClassVar[int] recordings: _containers.RepeatedCompositeFieldContainer[PAMRecording] hasMore: bool - def __init__(self, recordings: _Optional[_Iterable[_Union[PAMRecording, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... + def __init__(self, recordings: _Optional[_Iterable[_Union[PAMRecording, _Mapping]]] = ..., hasMore: _Optional[bool] = ...) -> None: ... class PAMData(_message.Message): __slots__ = ("vertex", "content") @@ -445,6 +498,14 @@ class UidList(_message.Message): uids: _containers.RepeatedScalarFieldContainer[bytes] def __init__(self, uids: _Optional[_Iterable[bytes]] = ...) -> None: ... +class PAMServiceNames(_message.Message): + __slots__ = ("resourceUid", "names") + RESOURCEUID_FIELD_NUMBER: _ClassVar[int] + NAMES_FIELD_NUMBER: _ClassVar[int] + resourceUid: bytes + names: bytes + def __init__(self, resourceUid: _Optional[bytes] = ..., names: _Optional[bytes] = ...) -> None: ... + class PAMResourceConfig(_message.Message): __slots__ = ("recordUid", "networkUid", "adminUid", "meta", "connectionSettings", "connectUsers", "domainUid", "jitSettings", "keeperAiSettings", "updateServices") RECORDUID_FIELD_NUMBER: _ClassVar[int] @@ -467,7 +528,7 @@ class PAMResourceConfig(_message.Message): jitSettings: bytes keeperAiSettings: bytes updateServices: bool - 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] = ..., keeperAiSettings: _Optional[bytes] = ..., updateServices: bool = ...) -> None: ... + 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] = ..., keeperAiSettings: _Optional[bytes] = ..., updateServices: _Optional[bool] = ...) -> None: ... class PAMUniversalSyncFolder(_message.Message): __slots__ = ("uid",) @@ -475,21 +536,41 @@ class PAMUniversalSyncFolder(_message.Message): uid: bytes def __init__(self, uid: _Optional[bytes] = ...) -> None: ... +class GitHubRepository(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: bytes + def __init__(self, name: _Optional[bytes] = ...) -> None: ... + +class GitHubConfig(_message.Message): + __slots__ = ("scope", "owner", "organizationVisibility", "repos") + SCOPE_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + ORGANIZATIONVISIBILITY_FIELD_NUMBER: _ClassVar[int] + REPOS_FIELD_NUMBER: _ClassVar[int] + scope: GitHubScope + owner: bytes + organizationVisibility: GitHubOrganizationVisibility + repos: _containers.RepeatedCompositeFieldContainer[GitHubRepository] + def __init__(self, scope: _Optional[_Union[GitHubScope, str]] = ..., owner: _Optional[bytes] = ..., organizationVisibility: _Optional[_Union[GitHubOrganizationVisibility, str]] = ..., repos: _Optional[_Iterable[_Union[GitHubRepository, _Mapping]]] = ...) -> None: ... + class PAMUniversalSyncConfig(_message.Message): - __slots__ = ("networkUid", "enabled", "dryRunEnabled", "folders", "syncIdentity", "vaultName") + __slots__ = ("networkUid", "enabled", "dryRunEnabled", "folders", "syncIdentity", "vaultName", "github") NETWORKUID_FIELD_NUMBER: _ClassVar[int] ENABLED_FIELD_NUMBER: _ClassVar[int] DRYRUNENABLED_FIELD_NUMBER: _ClassVar[int] FOLDERS_FIELD_NUMBER: _ClassVar[int] SYNCIDENTITY_FIELD_NUMBER: _ClassVar[int] VAULTNAME_FIELD_NUMBER: _ClassVar[int] + GITHUB_FIELD_NUMBER: _ClassVar[int] networkUid: bytes enabled: bool dryRunEnabled: bool folders: _containers.RepeatedCompositeFieldContainer[PAMUniversalSyncFolder] syncIdentity: bytes vaultName: bytes - def __init__(self, networkUid: _Optional[bytes] = ..., enabled: bool = ..., dryRunEnabled: bool = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ...) -> None: ... + github: GitHubConfig + def __init__(self, networkUid: _Optional[bytes] = ..., enabled: _Optional[bool] = ..., dryRunEnabled: _Optional[bool] = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ..., github: _Optional[_Union[GitHubConfig, _Mapping]] = ...) -> None: ... class NhiMetricsRequest(_message.Message): __slots__ = ("startTime", "endTime") @@ -564,14 +645,16 @@ class NhiBulkMetricsResponse(_message.Message): def __init__(self, responses: _Optional[_Iterable[_Union[NhiMetricsResponse, _Mapping]]] = ...) -> None: ... class NhiUidEntry(_message.Message): - __slots__ = ("uid", "category", "ksmNhi") + __slots__ = ("uid", "category", "ksmNhi", "appUid") UID_FIELD_NUMBER: _ClassVar[int] CATEGORY_FIELD_NUMBER: _ClassVar[int] KSMNHI_FIELD_NUMBER: _ClassVar[int] + APPUID_FIELD_NUMBER: _ClassVar[int] uid: str category: NhiCategory ksmNhi: bool - def __init__(self, uid: _Optional[str] = ..., category: _Optional[_Union[NhiCategory, str]] = ..., ksmNhi: bool = ...) -> None: ... + appUid: str + def __init__(self, uid: _Optional[str] = ..., category: _Optional[_Union[NhiCategory, str]] = ..., ksmNhi: _Optional[bool] = ..., appUid: _Optional[str] = ...) -> None: ... class GetNhiUidsRequest(_message.Message): __slots__ = ("startTime", "endTime") @@ -587,6 +670,20 @@ class GetNhiUidsResponse(_message.Message): uids: _containers.RepeatedCompositeFieldContainer[NhiUidEntry] def __init__(self, uids: _Optional[_Iterable[_Union[NhiUidEntry, _Mapping]]] = ...) -> None: ... +class SetNhiKsmEffectiveDateRequest(_message.Message): + __slots__ = ("effectiveDate",) + EFFECTIVEDATE_FIELD_NUMBER: _ClassVar[int] + effectiveDate: int + def __init__(self, effectiveDate: _Optional[int] = ...) -> None: ... + +class GetNhiKsmEffectiveDateResponse(_message.Message): + __slots__ = ("effectiveDate", "defaultDate") + EFFECTIVEDATE_FIELD_NUMBER: _ClassVar[int] + DEFAULTDATE_FIELD_NUMBER: _ClassVar[int] + effectiveDate: int + defaultDate: int + def __init__(self, effectiveDate: _Optional[int] = ..., defaultDate: _Optional[int] = ...) -> None: ... + class PAMUniversalSyncPreCheckRequest(_message.Message): __slots__ = ("networkUid", "folderUids") NETWORKUID_FIELD_NUMBER: _ClassVar[int] @@ -601,7 +698,7 @@ class PAMUniversalSyncPreCheckResult(_message.Message): ISUSED_FIELD_NUMBER: _ClassVar[int] folderUid: bytes isUsed: bool - def __init__(self, folderUid: _Optional[bytes] = ..., isUsed: bool = ...) -> None: ... + def __init__(self, folderUid: _Optional[bytes] = ..., isUsed: _Optional[bool] = ...) -> None: ... class PAMUniversalSyncPreCheckResponse(_message.Message): __slots__ = ("results",) diff --git a/unit-tests/pam/test_pam_nsf_config.py b/unit-tests/pam/test_pam_nsf_config.py index aa75e930c..30086837a 100644 --- a/unit-tests/pam/test_pam_nsf_config.py +++ b/unit-tests/pam/test_pam_nsf_config.py @@ -345,6 +345,98 @@ def parse_properties(_, record, **kwargs): params, 'config_uid', 'legacy_folder', command='pam-config-edit') +class TestPamConfigRequiredFieldValidation(unittest.TestCase): + """Regression tests: an empty required field (e.g. a GitHub config with no PAT) must abort + the whole operation instead of just logging a warning after the record was already saved.""" + + @staticmethod + def _make_missing_pat_field(): + field = vault.TypedField.new_field('secret', None, 'pamGitHubPersonalAccessToken') + field.required = True + return field + + @mock.patch('keepercommander.commands.discoveryrotation.create_pam_configuration_in_folder') + @mock.patch('keepercommander.commands.discoveryrotation.RecordEditMixin.get_record_type_fields', + return_value=[]) + def test_new_raises_and_does_not_create_record_when_required_field_empty( + self, mock_record_fields, mock_create_config): + params = _make_params() + folder = SharedFolderNode() + folder.uid = 'legacy_folder' + params.folder_cache[folder.uid] = folder + command = PAMConfigurationNewCommand() + + def parse_properties(_, record, **kwargs): + record.fields.append(vault.TypedField.new_field( + 'pamResources', {'folderUid': 'legacy_folder'})) + record.fields.append(self._make_missing_pat_field()) + + with mock.patch.object(command, 'parse_properties', side_effect=parse_properties): + with self.assertRaises(CommandError) as ctx: + command.execute(params, config_type='github', title='GitHub Config') + + self.assertIn('pamGitHubPersonalAccessToken', str(ctx.exception)) + mock_create_config.assert_not_called() + + @mock.patch('keepercommander.commands.discoveryrotation.update_pam_record') + @mock.patch('keepercommander.commands.discoveryrotation.RecordEditMixin.get_record_type_fields', + return_value=[]) + @mock.patch('keepercommander.vault.KeeperRecord.load') + def test_edit_raises_and_does_not_update_record_when_required_field_empty( + self, mock_load, mock_record_fields, mock_update_record): + params = _make_params() + folder = SharedFolderNode() + folder.uid = 'legacy_folder' + params.folder_cache[folder.uid] = folder + params.record_cache = {'config_uid': {}} + command = PAMConfigurationEditCommand() + + configuration = vault.TypedRecord(version=6) + configuration.record_uid = 'config_uid' + configuration.type_name = 'pamGitHubConfiguration' + configuration.fields.append(vault.TypedField.new_field( + 'pamResources', {'folderUid': 'legacy_folder'})) + mock_load.return_value = configuration + + def parse_properties(_, record, **kwargs): + record.fields.append(self._make_missing_pat_field()) + + with mock.patch.object(command, 'parse_properties', side_effect=parse_properties): + with self.assertRaises(CommandError) as ctx: + command.execute(params, uid='config_uid') + + self.assertIn('pamGitHubPersonalAccessToken', str(ctx.exception)) + mock_update_record.assert_not_called() + + +class TestPamConfigGitHubFieldMapping(unittest.TestCase): + """Regression test: --personal-access-token must land on the schema field the backend + actually marks required ("(secret).pamGitHubPersonalAccessToken"), not on a differently + labeled field, or the value is silently dropped and "pam config new" always aborts.""" + + def test_personal_access_token_populates_the_required_schema_field(self): + params = _make_params() + record = vault.TypedRecord(version=6) + record.type_name = 'pamGitHubConfiguration' + record.fields.append(vault.TypedField.new_field( + 'pamResources', {'folderUid': 'legacy_folder'})) + required_field = vault.TypedField.new_field('secret', None, 'pamGitHubPersonalAccessToken') + required_field.required = True + record.fields.append(required_field) + + command = PAMConfigurationNewCommand() + command.parse_properties( + params, record, github_id='github-test-acct', personal_access_token='github_pat_example') + + field = record.get_typed_field('secret', 'pamGitHubPersonalAccessToken') + self.assertIsNotNone(field) + self.assertEqual(field.get_default_value(str), 'github_pat_example') + # Regression guard: previously the mismatched label ("personalAccessToken" instead of + # "pamGitHubPersonalAccessToken") caused the value to be misfiled into a new custom field, + # leaving the actual required field empty. + self.assertIsNone(record.get_typed_field('secret', 'personalAccessToken')) + + class TestPamConfigRemoveNsf(unittest.TestCase): @mock.patch('keepercommander.vault.KeeperRecord.load') @@ -389,6 +481,21 @@ def test_resolve_pam_config_uid_finds_nsf_record_by_title(self, mock_load): params, 'PAM NSF Test Configuration') self.assertEqual(uid, 'config_uid') + @mock.patch('keepercommander.vault.KeeperRecord.load') + def test_resolve_pam_config_uid_finds_github_config_by_uid(self, mock_load): + # Regression test: pamGitHubConfiguration must be resolvable like every other + # PAM configuration type, or "pam config edit"/"pam config remove" can never find it. + params = _make_params() + params.record_cache = {'config_uid': {}} + + configuration = vault.TypedRecord(version=6) + configuration.record_uid = 'config_uid' + configuration.type_name = 'pamGitHubConfiguration' + mock_load.return_value = configuration + + uid = PAMConfigurationRemoveCommand._resolve_pam_config_uid(params, 'config_uid') + self.assertEqual(uid, 'config_uid') + @mock.patch('keepercommander.commands.pam.config_helper.RecordRemoveCommand') @mock.patch('keepercommander.commands.nested_share_folder.helpers.is_nested_share_record', return_value=False) diff --git a/unit-tests/pam/test_uss_github_config.py b/unit-tests/pam/test_uss_github_config.py new file mode 100644 index 000000000..cb05596f5 --- /dev/null +++ b/unit-tests/pam/test_uss_github_config.py @@ -0,0 +1,239 @@ +""" +Unit tests for the Universal Secrets Sync (USS) GitHub configuration fields on +`pam universal-sync-config add / edit / list`. + +Covers the nested `GitHubConfig` shape documented in krouter's +`docs/User API/ConfigureUniversalSync.md`: GitHub's scope/owner/organizationVisibility/repos +live under a single `github` message field on `PAMUniversalSyncConfig`, not as flat fields. +""" +import json +import unittest +from unittest import mock + +from keepercommander import crypto, utils, vault +from keepercommander.commands.universalsecretsync import ( + PAMUniversalSyncConfigAddCommand, + PAMUniversalSyncConfigEditCommand, + PAMUniversalSyncConfigListCommand, +) +from keepercommander.proto import pam_pb2 + +NETWORK_UID = 'AAAAAAAAAAAAAAAAAAAAAA' # roundtrip-safe base64url, 22 chars +RECORD_KEY = bytes(range(16)) +FAKE_TOKENS = (b'enc-session-token', b'enc-transmission-key', b'transmission-key') + + +def _make_network_record(record_type='pamGitHubConfiguration'): + network = vault.TypedRecord() + network.record_uid = NETWORK_UID + network.record_key = RECORD_KEY + network.title = 'GitHub Network' + network.type_name = record_type + return network + + +class FakeEdge: + def __init__(self, path, head_uid, content=None): + self.path = path + self.head_uid = head_uid + self._content = content + + @property + def content_as_dict(self): + return self._content + + +class FakeVertex: + def __init__(self, uid, edges=None, child_vertices=None): + self.uid = uid + self.edges = edges or [] + self._child_vertices = child_vertices or [] + + def has_vertices(self, *_args, **_kwargs): + return self._child_vertices + + +def _patch_dag(existing_config=None): + """Build mock DAG/Connection classes matching the lazy `from ..keeper_dag import DAG` + imports inside universalsecretsync.py. Returns (dag_class, connection_class, root).""" + root = FakeVertex(uid=NETWORK_UID) + if existing_config is not None: + root.edges = [FakeEdge('universal_sync', head_uid=NETWORK_UID, content=existing_config)] + + dag_instance = mock.MagicMock() + dag_instance.get_root = root + dag_instance.load.return_value = None + + dag_class = mock.MagicMock(return_value=dag_instance) + connection_class = mock.MagicMock() + return dag_class, connection_class, root + + +class TestPAMUniversalSyncConfigAddCommand(unittest.TestCase): + def setUp(self): + self.params = mock.MagicMock() + self.network = _make_network_record() + mock.patch('keepercommander.commands.universalsecretsync.get_keeper_tokens', + return_value=FAKE_TOKENS).start() + mock.patch('keepercommander.vault.KeeperRecord.load', return_value=self.network).start() + self.mock_router = mock.patch( + 'keepercommander.commands.universalsecretsync.router_helper.router_configure_universal_sync').start() + self.addCleanup(mock.patch.stopall) + + def _execute(self, **kwargs): + kwargs.setdefault('network', NETWORK_UID) + kwargs.setdefault('enabled', 'true') + kwargs.setdefault('dry_run', 'false') + PAMUniversalSyncConfigAddCommand().execute(self.params, **kwargs) + self.assertEqual(self.mock_router.call_count, 1) + return self.mock_router.call_args[0][1] + + def test_writes_github_config_under_nested_field(self): + rq = self._execute(scope='organization', owner='my-org', org_visibility='selected', + repo=['repo-a', 'repo-b']) + + self.assertTrue(rq.HasField('github')) + self.assertEqual(rq.github.scope, pam_pb2.ORGANIZATION) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.SELECTED) + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'my-org') + + self.assertEqual(len(rq.github.repos), 2) + decrypted_repos = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted_repos, ['repo-a', 'repo-b']) + + def test_repository_scope_maps_correctly(self): + rq = self._execute(scope='repository', owner='my-user') + self.assertEqual(rq.github.scope, pam_pb2.REPOSITORY) + + def test_no_github_flags_leaves_github_field_unset(self): + rq = self._execute() + self.assertFalse(rq.HasField('github')) + + def test_flat_fields_no_longer_exist_on_the_message(self): + rq = pam_pb2.PAMUniversalSyncConfig() + for flat_field in ('scope', 'owner', 'organizationVisibility', 'repos'): + with self.assertRaises(AttributeError): + getattr(rq, flat_field) + + +class TestPAMUniversalSyncConfigEditCommand(unittest.TestCase): + def setUp(self): + self.params = mock.MagicMock() + self.network = _make_network_record() + mock.patch('keepercommander.commands.universalsecretsync.get_keeper_tokens', + return_value=FAKE_TOKENS).start() + mock.patch('keepercommander.vault.KeeperRecord.load', return_value=self.network).start() + self.mock_router = mock.patch( + 'keepercommander.commands.universalsecretsync.router_helper.router_configure_universal_sync').start() + self.addCleanup(mock.patch.stopall) + + @staticmethod + def _existing_github_config(repo_names, scope=pam_pb2.REPOSITORY, org_visibility=pam_pb2.PRIVATE, + owner='existing-owner'): + encrypted_owner = crypto.encrypt_aes_v2(owner.encode('utf-8'), RECORD_KEY) + repos_b64 = [utils.base64_url_encode(crypto.encrypt_aes_v2(name.encode('utf-8'), RECORD_KEY)) + for name in repo_names] + return { + 'scope': scope, + 'owner': utils.base64_url_encode(encrypted_owner), + 'organizationVisibility': org_visibility, + 'repos': repos_b64, + } + + def _execute_with_dag(self, existing_config, **kwargs): + dag_class, connection_class, _root = _patch_dag(existing_config) + kwargs.setdefault('network', NETWORK_UID) + with mock.patch('keepercommander.keeper_dag.DAG', dag_class), \ + mock.patch('keepercommander.keeper_dag.connection.commander.Connection', connection_class): + PAMUniversalSyncConfigEditCommand().execute(self.params, **kwargs) + # Guard against the broad try/except in the command silently swallowing a bad mock wire-up. + self.assertEqual(dag_class.call_count, 1) + self.assertEqual(self.mock_router.call_count, 1) + return self.mock_router.call_args[0][1] + + def test_preserves_existing_github_config_when_not_overridden(self): + existing = {'github': self._existing_github_config(['repo-1', 'repo-2'])} + rq = self._execute_with_dag(existing) + + self.assertTrue(rq.HasField('github')) + self.assertEqual(rq.github.scope, pam_pb2.REPOSITORY) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.PRIVATE) + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'existing-owner') + decrypted = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted, ['repo-1', 'repo-2']) + + def test_overrides_take_precedence_over_existing(self): + existing = {'github': self._existing_github_config(['old-repo'])} + rq = self._execute_with_dag(existing, scope='organization', owner='new-owner', + org_visibility='all', repo=['new-repo']) + + self.assertEqual(rq.github.scope, pam_pb2.ORGANIZATION) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.ALL) + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'new-owner') + decrypted = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted, ['new-repo']) + + def test_partial_override_preserves_the_rest(self): + # Only --owner is overridden; scope/org-visibility/repos should carry forward. + existing = {'github': self._existing_github_config(['repo-1'], scope=pam_pb2.ORGANIZATION, + org_visibility=pam_pb2.SELECTED)} + rq = self._execute_with_dag(existing, owner='new-owner') + + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'new-owner') + self.assertEqual(rq.github.scope, pam_pb2.ORGANIZATION) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.SELECTED) + decrypted = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted, ['repo-1']) + + def test_no_existing_and_no_override_leaves_github_unset(self): + rq = self._execute_with_dag(None) + self.assertFalse(rq.HasField('github')) + + +class TestPAMUniversalSyncConfigListDetails(unittest.TestCase): + def setUp(self): + self.params = mock.MagicMock() + self.params.folder_cache = {} + self.params.subfolder_cache = {} + self.network = _make_network_record() + mock.patch('keepercommander.vault.KeeperRecord.load', return_value=self.network).start() + self.addCleanup(mock.patch.stopall) + + def _print_details(self, existing_config, format_type='json'): + dag_class, connection_class, _root = _patch_dag(existing_config) + with mock.patch('keepercommander.keeper_dag.DAG', dag_class), \ + mock.patch('keepercommander.keeper_dag.connection.commander.Connection', connection_class): + return PAMUniversalSyncConfigListCommand.print_uss_configuration_details( + self.params, NETWORK_UID, format_type=format_type) + + def test_json_details_reads_nested_github_fields(self): + encrypted_owner = crypto.encrypt_aes_v2(b'my-org', RECORD_KEY) + encrypted_repo = crypto.encrypt_aes_v2(b'my-repo', RECORD_KEY) + existing_config = { + 'enabled': True, + 'github': { + 'scope': pam_pb2.ORGANIZATION, + 'owner': utils.base64_url_encode(encrypted_owner), + 'organizationVisibility': pam_pb2.ALL, + 'repos': [utils.base64_url_encode(encrypted_repo)], + }, + } + + result = json.loads(self._print_details(existing_config)) + + self.assertEqual(result['scope'], 'ORGANIZATION') + self.assertEqual(result['organization_visibility'], 'ALL') + self.assertEqual(result['owner'], 'my-org') + self.assertEqual(result['repos'], ['my-repo']) + + def test_json_details_without_github_config(self): + result = json.loads(self._print_details({'enabled': True})) + + self.assertEqual(result['scope'], 'N/A') + self.assertEqual(result['organization_visibility'], 'N/A') + self.assertEqual(result['owner'], 'N/A') + self.assertEqual(result['repos'], []) + + +if __name__ == '__main__': + unittest.main()